@@ -8,7 +8,7 @@ use std::marker::PhantomData;
88use std:: time:: Instant ;
99
1010use backoff:: backoff:: Backoff as _;
11- use futures:: { SinkExt as _, StreamExt as _} ;
11+ use futures:: { FutureExt as _ , SinkExt as _, StreamExt as _} ;
1212use serde:: Serialize ;
1313use serde:: de:: DeserializeOwned ;
1414use tokio:: net:: TcpStream ;
@@ -29,6 +29,23 @@ type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
2929/// Broadcast channel capacity for incoming messages.
3030const BROADCAST_CAPACITY : usize = 1024 ;
3131
32+ /// Install a default rustls crypto provider if the process does not already have one.
33+ ///
34+ /// Establishing the TLS connection resolves the process-level rustls `CryptoProvider`
35+ /// from crate features, which panics when more than one provider is present in the
36+ /// dependency graph. That happens readily in practice, because an application only has
37+ /// to pull in `ring` alongside the `aws-lc-rs` that rustls enables by default. Installing
38+ /// a provider explicitly avoids the panic, and an application that has already installed
39+ /// its own provider is left untouched.
40+ fn ensure_crypto_provider ( ) {
41+ static INIT : std:: sync:: Once = std:: sync:: Once :: new ( ) ;
42+ INIT . call_once ( || {
43+ if rustls:: crypto:: CryptoProvider :: get_default ( ) . is_none ( ) {
44+ _ = rustls:: crypto:: aws_lc_rs:: default_provider ( ) . install_default ( ) ;
45+ }
46+ } ) ;
47+ }
48+
3249/// Connection state tracking.
3350#[ non_exhaustive]
3451#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
@@ -119,22 +136,36 @@ where
119136 let ( broadcast_tx, _) = broadcast:: channel ( BROADCAST_CAPACITY ) ;
120137 let ( state_tx, state_rx) = watch:: channel ( ConnectionState :: Disconnected ) ;
121138
139+ ensure_crypto_provider ( ) ;
140+
122141 // Spawn connection task
123142 let connection_config = config;
124143 let connection_endpoint = endpoint;
125144 let broadcast_tx_clone = broadcast_tx. clone ( ) ;
126145 let state_tx_clone = state_tx. clone ( ) ;
146+ let panic_state_tx = state_tx. clone ( ) ;
127147
128148 tokio:: spawn ( async move {
129- Self :: connection_loop (
149+ let connection_loop = std :: panic :: AssertUnwindSafe ( Self :: connection_loop (
130150 connection_endpoint,
131151 connection_config,
132152 sender_rx,
133153 broadcast_tx_clone,
134154 parser,
135155 state_tx_clone,
136- )
137- . await ;
156+ ) ) ;
157+
158+ if connection_loop. catch_unwind ( ) . await . is_err ( ) {
159+ // This task is detached, so a panic in the connection loop is otherwise
160+ // swallowed: the manager would keep reporting whatever state it reached
161+ // last (typically `Connecting`) and every subscription stream would hang
162+ // with no error and no data. Report the connection as disconnected so the
163+ // failure is at least observable.
164+ _ = panic_state_tx. send ( ConnectionState :: Disconnected ) ;
165+
166+ #[ cfg( feature = "tracing" ) ]
167+ tracing:: error!( "websocket connection task panicked; reporting disconnected" ) ;
168+ }
138169 } ) ;
139170
140171 Ok ( Self {
@@ -424,3 +455,17 @@ where
424455 self . state_tx . subscribe ( )
425456 }
426457}
458+
459+ #[ cfg( test) ]
460+ mod tests {
461+ use super :: ensure_crypto_provider;
462+
463+ #[ test]
464+ fn ensure_crypto_provider_installs_a_default ( ) {
465+ // More than one rustls crypto provider is present in the dependency graph, so
466+ // the process-level default cannot be resolved from crate features and setting
467+ // up TLS panics. Installing one keeps that from happening.
468+ ensure_crypto_provider ( ) ;
469+ assert ! ( rustls:: crypto:: CryptoProvider :: get_default( ) . is_some( ) ) ;
470+ }
471+ }
0 commit comments