stream_tungstenite/
error.rs

1//! Unified error types for the WebSocket client library.
2
3use std::time::Duration;
4
5/// Top-level client error
6#[derive(thiserror::Error, Debug)]
7pub enum ClientError {
8    #[error("Connection failed: {0}")]
9    Connect(#[from] ConnectError),
10
11    #[error("Handshake failed: {0}")]
12    Handshake(#[from] HandshakeError),
13
14    #[error("Send failed: {0}")]
15    Send(#[from] SendError),
16
17    #[error("Receive failed: {0}")]
18    Receive(#[from] ReceiveError),
19
20    #[error("Supervisor error: {0}")]
21    Supervisor(#[from] SupervisorError),
22
23    #[error("Extension error: {0}")]
24    Extension(#[from] ExtensionError),
25
26    #[error("Invalid configuration: {0}")]
27    Config(String),
28
29    #[error("Client is already running")]
30    AlreadyRunning,
31
32    /// Graceful shutdown did not complete within the specified timeout
33    #[error("Graceful shutdown timed out after {0:?}")]
34    ShutdownTimeout(Duration),
35
36    /// Error with additional context (preserves source error)
37    #[error("{context}: {source}")]
38    Context {
39        /// Human-readable context message
40        context: String,
41        /// Original error as the source of this error
42        #[source]
43        source: Box<ClientError>,
44    },
45}
46
47/// Connection-related errors
48#[derive(thiserror::Error, Debug, Clone)]
49pub enum ConnectError {
50    #[error("Invalid URL: {0}")]
51    InvalidUrl(String),
52
53    #[error("Invalid URI: {0}")]
54    InvalidUri(String),
55
56    #[error("DNS resolution failed: {0}")]
57    DnsResolution(String),
58
59    #[error("TCP connection failed: {0}")]
60    TcpConnect(String),
61
62    #[error("TCP failed: {0}")]
63    TcpFailed(String),
64
65    #[error("TLS handshake failed: {0}")]
66    TlsHandshake(String),
67
68    #[error("TLS configuration error: {0}")]
69    Tls(String),
70
71    #[error("WebSocket upgrade failed: {0}")]
72    WebSocketUpgrade(String),
73
74    #[error("WebSocket connection failed: {0}")]
75    WebSocketFailed(String),
76
77    #[error("Handshake failed: {0}")]
78    HandshakeFailed(String),
79
80    #[error("Connection timeout after {0:?}")]
81    Timeout(Duration),
82
83    #[error("Connection refused")]
84    Refused,
85
86    #[error("IO error: {0}")]
87    Io(String),
88}
89
90impl ConnectError {
91    /// Whether this error is retryable
92    #[must_use]
93    pub const fn is_retryable(&self) -> bool {
94        match self {
95            // Not retryable - configuration/permanent errors
96            Self::InvalidUrl(_) | Self::InvalidUri(_) | Self::TlsHandshake(_) | Self::Tls(_) => {
97                false
98            } // Certificate issues won't fix themselves
99
100            // Retryable - transient errors
101            Self::DnsResolution(_)
102            | Self::TcpConnect(_)
103            | Self::TcpFailed(_)
104            | Self::WebSocketUpgrade(_)
105            | Self::WebSocketFailed(_)
106            | Self::HandshakeFailed(_)
107            | Self::Timeout(_)
108            | Self::Refused
109            | Self::Io(_) => true,
110        }
111    }
112
113    /// Suggested retry delay for this error type
114    #[must_use]
115    pub const fn suggested_delay(&self) -> Option<Duration> {
116        match self {
117            Self::DnsResolution(_) => Some(Duration::from_millis(500)),
118            Self::Timeout(_) => Some(Duration::from_secs(5)),
119            Self::Refused => Some(Duration::from_secs(2)),
120            _ => None, // Use default backoff
121        }
122    }
123}
124
125impl From<std::io::Error> for ConnectError {
126    fn from(e: std::io::Error) -> Self {
127        Self::Io(e.to_string())
128    }
129}
130
131impl From<tungstenite::Error> for ConnectError {
132    fn from(e: tungstenite::Error) -> Self {
133        Self::WebSocketUpgrade(e.to_string())
134    }
135}
136
137/// Handshake-related errors
138#[derive(thiserror::Error, Debug, Clone)]
139pub enum HandshakeError {
140    #[error("Handshake failed: {0}")]
141    Failed(String),
142
143    #[error("Authentication failed: {0}")]
144    AuthFailed(String),
145
146    #[error("Handshake timeout after {0:?}")]
147    Timeout(Duration),
148
149    #[error("Protocol error: {0}")]
150    Protocol(String),
151
152    #[error("WebSocket error: {0}")]
153    WebSocket(String),
154}
155
156impl HandshakeError {
157    /// Whether this handshake error is retryable
158    #[must_use]
159    pub const fn is_retryable(&self) -> bool {
160        match self {
161            // Not retryable - auth/protocol issues are permanent
162            Self::AuthFailed(_) | Self::Protocol(_) => false,
163            // Retryable - transient errors
164            Self::Failed(_) | Self::Timeout(_) | Self::WebSocket(_) => true,
165        }
166    }
167}
168
169impl From<tungstenite::Error> for HandshakeError {
170    fn from(e: tungstenite::Error) -> Self {
171        Self::WebSocket(e.to_string())
172    }
173}
174
175/// Send-related errors
176#[derive(thiserror::Error, Debug, Clone)]
177pub enum SendError {
178    #[error("Not connected")]
179    NotConnected,
180
181    #[error("Channel closed")]
182    ChannelClosed,
183
184    #[error("Send buffer is full")]
185    ChannelFull,
186
187    #[error("Send timed out after {0:?}")]
188    Timeout(Duration),
189
190    #[error("Message too large: {size} bytes (max: {max})")]
191    MessageTooLarge { size: usize, max: usize },
192
193    #[error("WebSocket error: {0}")]
194    WebSocket(String),
195}
196
197impl From<tungstenite::Error> for SendError {
198    fn from(e: tungstenite::Error) -> Self {
199        Self::WebSocket(e.to_string())
200    }
201}
202
203/// Receive-related errors
204#[derive(thiserror::Error, Debug, Clone)]
205pub enum ReceiveError {
206    #[error("Stream closed")]
207    StreamClosed,
208
209    #[error("Receive timeout after {0:?}")]
210    Timeout(Duration),
211
212    #[error("WebSocket error: {0}")]
213    WebSocket(String),
214}
215
216impl From<tungstenite::Error> for ReceiveError {
217    fn from(e: tungstenite::Error) -> Self {
218        Self::WebSocket(e.to_string())
219    }
220}
221
222/// Connection supervisor errors
223#[derive(thiserror::Error, Debug, Clone)]
224pub enum SupervisorError {
225    #[error("Max retries exceeded after {attempts} attempts")]
226    MaxRetriesExceeded { attempts: u32 },
227
228    #[error("Shutdown requested")]
229    Shutdown,
230
231    #[error("Fatal error: {0}")]
232    Fatal(String),
233}
234
235/// Extension-related errors
236#[derive(thiserror::Error, Debug, Clone)]
237pub enum ExtensionError {
238    #[error("Extension '{name}' failed: {message}")]
239    Failed { name: String, message: String },
240
241    #[error("Extension '{name}' initialization failed: {message}")]
242    InitFailed { name: String, message: String },
243}
244
245/// Disconnect reason
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum DisconnectReason {
248    /// Normal closure
249    Normal,
250    /// Connection error
251    Error(String),
252    /// Receive timeout
253    Timeout,
254    /// Graceful shutdown requested
255    Shutdown,
256    /// Server closed connection
257    ServerClosed {
258        code: Option<u16>,
259        reason: Option<String>,
260    },
261}
262
263impl std::fmt::Display for DisconnectReason {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        match self {
266            Self::Normal => write!(f, "normal closure"),
267            Self::Error(e) => write!(f, "error: {e}"),
268            Self::Timeout => write!(f, "timeout"),
269            Self::Shutdown => write!(f, "shutdown"),
270            Self::ServerClosed { code, reason } => {
271                write!(f, "server closed")?;
272                if let Some(c) = code {
273                    write!(f, " (code: {c})")?;
274                }
275                if let Some(r) = reason {
276                    write!(f, ": {r}")?;
277                }
278                Ok(())
279            }
280        }
281    }
282}
283
284/// Result type alias for client operations
285pub type ClientResult<T> = Result<T, ClientError>;
286
287/// Helper trait for converting errors with context
288pub trait ErrorContext<T> {
289    /// Converts this result, adding context to any error.
290    fn with_context(self, context: impl Into<String>) -> Result<T, ClientError>;
291}
292
293impl<T, E: Into<ClientError>> ErrorContext<T> for Result<T, E> {
294    fn with_context(self, context: impl Into<String>) -> Result<T, ClientError> {
295        self.map_err(|e| ClientError::Context {
296            context: context.into(),
297            source: Box::new(e.into()),
298        })
299    }
300}