sockudo_ws/
error.rs

1//! Error types for the WebSocket library
2
3use std::fmt;
4use std::io;
5
6/// Result type alias for WebSocket operations
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// WebSocket error types
10#[derive(Debug)]
11pub enum Error {
12    /// I/O error from the underlying socket
13    Io(io::Error),
14    /// Invalid WebSocket frame
15    InvalidFrame(&'static str),
16    /// Invalid UTF-8 in text message
17    InvalidUtf8,
18    /// Protocol violation
19    Protocol(&'static str),
20    /// Connection closed normally
21    ConnectionClosed,
22    /// Message too large
23    MessageTooLarge,
24    /// Frame too large
25    FrameTooLarge,
26    /// Invalid HTTP request
27    InvalidHttp(&'static str),
28    /// Handshake failed
29    HandshakeFailed(&'static str),
30    /// Buffer full (backpressure)
31    BufferFull,
32    /// Would block (non-blocking I/O)
33    WouldBlock,
34    /// Connection reset by peer
35    ConnectionReset,
36    /// Invalid state
37    InvalidState(&'static str),
38    /// Close frame received
39    Closed(Option<CloseReason>),
40    /// Invalid close code
41    InvalidCloseCode(u16),
42    /// Capacity exceeded
43    Capacity(&'static str),
44    /// Compression/decompression error
45    Compression(String),
46
47    // ========================================================================
48    // Transport-specific errors
49    // ========================================================================
50    /// HTTP/2 error
51    #[cfg(feature = "http2")]
52    Http2(h2::Error),
53
54    /// HTTP/3 error
55    #[cfg(feature = "http3")]
56    Http3(String),
57
58    /// QUIC connection error
59    #[cfg(feature = "http3")]
60    Quic(quinn::ConnectionError),
61
62    /// QUIC write error
63    #[cfg(feature = "http3")]
64    QuicWrite(quinn::WriteError),
65
66    /// Extended CONNECT protocol not supported by server
67    ExtendedConnectNotSupported,
68
69    /// Stream was reset by peer
70    StreamReset,
71}
72
73/// Close frame reason
74#[derive(Debug, Clone)]
75pub struct CloseReason {
76    /// Close status code
77    pub code: u16,
78    /// Optional reason string
79    pub reason: String,
80}
81
82impl CloseReason {
83    /// Normal closure
84    pub const NORMAL: u16 = 1000;
85    /// Going away (e.g., server shutdown)
86    pub const GOING_AWAY: u16 = 1001;
87    /// Protocol error
88    pub const PROTOCOL_ERROR: u16 = 1002;
89    /// Unsupported data
90    pub const UNSUPPORTED: u16 = 1003;
91    /// No status received
92    pub const NO_STATUS: u16 = 1005;
93    /// Abnormal closure
94    pub const ABNORMAL: u16 = 1006;
95    /// Invalid frame payload
96    pub const INVALID_PAYLOAD: u16 = 1007;
97    /// Policy violation
98    pub const POLICY: u16 = 1008;
99    /// Message too big
100    pub const TOO_BIG: u16 = 1009;
101    /// Mandatory extension
102    pub const EXTENSION: u16 = 1010;
103    /// Internal server error
104    pub const INTERNAL: u16 = 1011;
105
106    /// Create a new close reason
107    pub fn new(code: u16, reason: impl Into<String>) -> Self {
108        Self {
109            code,
110            reason: reason.into(),
111        }
112    }
113
114    /// Check if the close code is valid per RFC 6455
115    pub fn is_valid_code(code: u16) -> bool {
116        matches!(code, 1000..=1003 | 1007..=1011 | 3000..=4999)
117    }
118}
119
120// ============================================================================
121// Error Categorization
122// ============================================================================
123
124/// Error category for classification and metrics
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub enum ErrorKind {
127    /// I/O and connection errors
128    Io,
129    /// WebSocket protocol violations
130    Protocol,
131    /// Handshake failures
132    Handshake,
133    /// Data validation errors (UTF-8, frame structure)
134    Validation,
135    /// Resource limits exceeded (frame/message size)
136    Capacity,
137    /// Compression/decompression errors
138    Compression,
139    /// Connection state errors
140    Connection,
141    /// Transport-specific errors (HTTP/2, HTTP/3, QUIC)
142    Transport,
143}
144
145impl Error {
146    /// Get the error category/kind
147    ///
148    /// Useful for metrics, logging, and error handling decisions.
149    pub fn kind(&self) -> ErrorKind {
150        match self {
151            Error::Io(_) => ErrorKind::Io,
152            Error::InvalidFrame(_) => ErrorKind::Validation,
153            Error::InvalidUtf8 => ErrorKind::Validation,
154            Error::Protocol(_) => ErrorKind::Protocol,
155            Error::ConnectionClosed => ErrorKind::Connection,
156            Error::MessageTooLarge => ErrorKind::Capacity,
157            Error::FrameTooLarge => ErrorKind::Capacity,
158            Error::InvalidHttp(_) => ErrorKind::Handshake,
159            Error::HandshakeFailed(_) => ErrorKind::Handshake,
160            Error::BufferFull => ErrorKind::Capacity,
161            Error::WouldBlock => ErrorKind::Io,
162            Error::ConnectionReset => ErrorKind::Connection,
163            Error::InvalidState(_) => ErrorKind::Protocol,
164            Error::Closed(_) => ErrorKind::Connection,
165            Error::InvalidCloseCode(_) => ErrorKind::Validation,
166            Error::Capacity(_) => ErrorKind::Capacity,
167            Error::Compression(_) => ErrorKind::Compression,
168            #[cfg(feature = "http2")]
169            Error::Http2(_) => ErrorKind::Transport,
170            #[cfg(feature = "http3")]
171            Error::Http3(_) => ErrorKind::Transport,
172            #[cfg(feature = "http3")]
173            Error::Quic(_) => ErrorKind::Transport,
174            #[cfg(feature = "http3")]
175            Error::QuicWrite(_) => ErrorKind::Transport,
176            Error::ExtendedConnectNotSupported => ErrorKind::Handshake,
177            Error::StreamReset => ErrorKind::Connection,
178        }
179    }
180
181    /// Check if this error is fatal (connection cannot continue)
182    ///
183    /// Fatal errors indicate the connection is broken and cannot be recovered.
184    /// Non-fatal errors may allow the connection to continue after handling.
185    #[inline]
186    pub fn is_fatal(&self) -> bool {
187        matches!(
188            self,
189            Error::ConnectionClosed
190                | Error::ConnectionReset
191                | Error::Protocol(_)
192                | Error::InvalidUtf8
193                | Error::FrameTooLarge
194                | Error::MessageTooLarge
195                | Error::StreamReset
196                | Error::InvalidCloseCode(_)
197        )
198    }
199
200    /// Check if this error is recoverable
201    ///
202    /// Recoverable errors are transient and the operation may succeed if retried.
203    #[inline]
204    pub fn is_recoverable(&self) -> bool {
205        matches!(self, Error::WouldBlock | Error::BufferFull)
206    }
207
208    /// Check if this error is a timeout
209    ///
210    /// Note: Timeout errors are typically wrapped by the caller using
211    /// `tokio::time::timeout`, but some transport errors may indicate timeouts.
212    #[inline]
213    pub fn is_timeout(&self) -> bool {
214        if let Error::Io(e) = self {
215            return e.kind() == io::ErrorKind::TimedOut;
216        }
217        false
218    }
219
220    /// Check if this error is a connection error
221    #[inline]
222    pub fn is_connection_error(&self) -> bool {
223        self.kind() == ErrorKind::Connection
224    }
225
226    /// Check if this error is a protocol error
227    #[inline]
228    pub fn is_protocol_error(&self) -> bool {
229        self.kind() == ErrorKind::Protocol
230    }
231
232    /// Get a metric-friendly name for this error
233    ///
234    /// Returns a short, lowercase, underscore-separated string suitable
235    /// for use in metrics labels.
236    pub fn metric_name(&self) -> &'static str {
237        match self {
238            Error::Io(_) => "io_error",
239            Error::InvalidFrame(_) => "invalid_frame",
240            Error::InvalidUtf8 => "invalid_utf8",
241            Error::Protocol(_) => "protocol_error",
242            Error::ConnectionClosed => "connection_closed",
243            Error::MessageTooLarge => "message_too_large",
244            Error::FrameTooLarge => "frame_too_large",
245            Error::InvalidHttp(_) => "invalid_http",
246            Error::HandshakeFailed(_) => "handshake_failed",
247            Error::BufferFull => "buffer_full",
248            Error::WouldBlock => "would_block",
249            Error::ConnectionReset => "connection_reset",
250            Error::InvalidState(_) => "invalid_state",
251            Error::Closed(_) => "closed",
252            Error::InvalidCloseCode(_) => "invalid_close_code",
253            Error::Capacity(_) => "capacity_exceeded",
254            Error::Compression(_) => "compression_error",
255            #[cfg(feature = "http2")]
256            Error::Http2(_) => "http2_error",
257            #[cfg(feature = "http3")]
258            Error::Http3(_) => "http3_error",
259            #[cfg(feature = "http3")]
260            Error::Quic(_) => "quic_error",
261            #[cfg(feature = "http3")]
262            Error::QuicWrite(_) => "quic_write_error",
263            Error::ExtendedConnectNotSupported => "extended_connect_not_supported",
264            Error::StreamReset => "stream_reset",
265        }
266    }
267
268    /// Get a suggested HTTP status code for this error
269    ///
270    /// Useful when converting WebSocket errors to HTTP responses
271    /// during handshake or in HTTP/2/HTTP/3 contexts.
272    pub fn suggested_http_status(&self) -> u16 {
273        match self {
274            Error::InvalidHttp(_) | Error::InvalidFrame(_) | Error::InvalidCloseCode(_) => 400, // Bad Request
275            Error::Protocol(_) | Error::InvalidUtf8 => 400, // Bad Request
276            Error::HandshakeFailed(_) => 400,               // Bad Request
277            Error::MessageTooLarge | Error::FrameTooLarge | Error::Capacity(_) => 413, // Payload Too Large
278            Error::BufferFull => 503, // Service Unavailable
279            Error::ExtendedConnectNotSupported => 501, // Not Implemented
280            _ => 500,                 // Internal Server Error
281        }
282    }
283}
284
285impl fmt::Display for Error {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        match self {
288            Error::Io(e) => write!(f, "I/O error: {}", e),
289            Error::InvalidFrame(msg) => write!(f, "Invalid frame: {}", msg),
290            Error::InvalidUtf8 => write!(f, "Invalid UTF-8 in text message"),
291            Error::Protocol(msg) => write!(f, "Protocol error: {}", msg),
292            Error::ConnectionClosed => write!(f, "Connection closed"),
293            Error::MessageTooLarge => write!(f, "Message too large"),
294            Error::FrameTooLarge => write!(f, "Frame too large"),
295            Error::InvalidHttp(msg) => write!(f, "Invalid HTTP: {}", msg),
296            Error::HandshakeFailed(msg) => write!(f, "Handshake failed: {}", msg),
297            Error::BufferFull => write!(f, "Buffer full"),
298            Error::WouldBlock => write!(f, "Would block"),
299            Error::ConnectionReset => write!(f, "Connection reset by peer"),
300            Error::InvalidState(msg) => write!(f, "Invalid state: {}", msg),
301            Error::Closed(reason) => {
302                if let Some(r) = reason {
303                    write!(f, "Connection closed: {} ({})", r.code, r.reason)
304                } else {
305                    write!(f, "Connection closed")
306                }
307            }
308            Error::InvalidCloseCode(code) => write!(f, "Invalid close code: {}", code),
309            Error::Capacity(msg) => write!(f, "Capacity exceeded: {}", msg),
310            Error::Compression(msg) => write!(f, "Compression error: {}", msg),
311            #[cfg(feature = "http2")]
312            Error::Http2(e) => write!(f, "HTTP/2 error: {}", e),
313            #[cfg(feature = "http3")]
314            Error::Http3(msg) => write!(f, "HTTP/3 error: {}", msg),
315            #[cfg(feature = "http3")]
316            Error::Quic(e) => write!(f, "QUIC error: {}", e),
317            #[cfg(feature = "http3")]
318            Error::QuicWrite(e) => write!(f, "QUIC write error: {}", e),
319            Error::ExtendedConnectNotSupported => {
320                write!(f, "Extended CONNECT protocol not supported by server")
321            }
322            Error::StreamReset => write!(f, "Stream was reset by peer"),
323        }
324    }
325}
326
327impl std::error::Error for Error {
328    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
329        match self {
330            Error::Io(e) => Some(e),
331            #[cfg(feature = "http2")]
332            Error::Http2(e) => Some(e),
333            #[cfg(feature = "http3")]
334            Error::Quic(e) => Some(e),
335            #[cfg(feature = "http3")]
336            Error::QuicWrite(e) => Some(e),
337            _ => None,
338        }
339    }
340}
341
342impl From<io::Error> for Error {
343    fn from(e: io::Error) -> Self {
344        match e.kind() {
345            io::ErrorKind::WouldBlock => Error::WouldBlock,
346            io::ErrorKind::ConnectionReset => Error::ConnectionReset,
347            io::ErrorKind::BrokenPipe => Error::ConnectionClosed,
348            io::ErrorKind::UnexpectedEof => Error::ConnectionClosed,
349            _ => Error::Io(e),
350        }
351    }
352}
353
354impl From<Error> for io::Error {
355    fn from(e: Error) -> Self {
356        match e {
357            Error::Io(e) => e,
358            Error::WouldBlock => io::Error::new(io::ErrorKind::WouldBlock, "would block"),
359            Error::ConnectionReset => {
360                io::Error::new(io::ErrorKind::ConnectionReset, "connection reset")
361            }
362            Error::ConnectionClosed => {
363                io::Error::new(io::ErrorKind::BrokenPipe, "connection closed")
364            }
365            other => io::Error::other(other.to_string()),
366        }
367    }
368}
369
370// ============================================================================
371// Transport-specific From implementations
372// ============================================================================
373
374#[cfg(feature = "http2")]
375impl From<h2::Error> for Error {
376    fn from(e: h2::Error) -> Self {
377        if e.is_io() {
378            // Clone the error info before consuming it
379            let err_string = e.to_string();
380            if let Some(io_err) = e.into_io() {
381                return Error::Io(io_err);
382            }
383            // If into_io returned None despite is_io being true, wrap as generic IO error
384            return Error::Io(std::io::Error::other(err_string));
385        }
386        Error::Http2(e)
387    }
388}
389
390#[cfg(feature = "http3")]
391impl From<quinn::ConnectionError> for Error {
392    fn from(e: quinn::ConnectionError) -> Self {
393        Error::Quic(e)
394    }
395}
396
397#[cfg(feature = "http3")]
398impl From<quinn::WriteError> for Error {
399    fn from(e: quinn::WriteError) -> Self {
400        Error::QuicWrite(e)
401    }
402}
403
404#[cfg(feature = "http3")]
405impl From<h3::error::ConnectionError> for Error {
406    fn from(e: h3::error::ConnectionError) -> Self {
407        Error::Http3(e.to_string())
408    }
409}
410
411#[cfg(feature = "http3")]
412impl From<h3::error::StreamError> for Error {
413    fn from(e: h3::error::StreamError) -> Self {
414        Error::Http3(e.to_string())
415    }
416}