Skip to main content

nexus_web/ws/
error.rs

1/// Protocol error from WebSocket frame decoding.
2///
3/// Each variant is a specific RFC 6455 violation. No catch-all.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum ProtocolError {
6    /// Frame header contains an unrecognized opcode.
7    InvalidOpcode(u8),
8    /// Reserved bits (RSV1-3) are set without a negotiated extension.
9    ReservedBitsSet {
10        /// The RSV bits that were set (bits 4-6 of byte 0).
11        bits: u8,
12    },
13    /// Server sent a masked frame (RFC 6455 §5.1: server MUST NOT mask).
14    MaskedFrameFromServer,
15    /// Client sent an unmasked frame (RFC 6455 §5.1: client MUST mask).
16    UnmaskedFrameFromClient,
17    /// Frame payload exceeds the configured maximum frame size.
18    PayloadTooLarge {
19        /// Declared payload size.
20        size: u64,
21        /// Configured maximum.
22        max: u64,
23    },
24    /// Control frame payload exceeds 125 bytes (RFC 6455 §5.5).
25    ControlFrameTooLarge {
26        /// Declared payload size.
27        size: u64,
28    },
29    /// Control frame is fragmented (RFC 6455 §5.5: MUST NOT be fragmented).
30    FragmentedControlFrame,
31    /// Close frame has invalid status code.
32    InvalidCloseCode(u16),
33    /// Close frame reason is not valid UTF-8.
34    InvalidUtf8InCloseReason,
35    /// Close frame payload is 1 byte (must be 0 or >= 2).
36    CloseFrameTooShort,
37    /// Received a continuation frame with no preceding start frame.
38    ContinuationWithoutStart,
39    /// Received a new data frame (Text/Binary) while assembling fragments.
40    NewMessageDuringAssembly,
41    /// Text message payload is not valid UTF-8.
42    InvalidUtf8,
43    /// Assembled message exceeds the configured maximum message size.
44    MessageTooLarge {
45        /// Accumulated size so far.
46        accumulated: usize,
47        /// Configured maximum.
48        max: usize,
49    },
50}
51
52impl std::fmt::Display for ProtocolError {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::InvalidOpcode(op) => write!(f, "invalid opcode: 0x{op:X}"),
56            Self::ReservedBitsSet { bits } => {
57                write!(f, "reserved bits set: 0b{bits:03b}")
58            }
59            Self::MaskedFrameFromServer => write!(f, "server sent masked frame"),
60            Self::UnmaskedFrameFromClient => write!(f, "client sent unmasked frame"),
61            Self::PayloadTooLarge { size, max } => {
62                write!(f, "payload too large: {size} bytes (max {max})")
63            }
64            Self::ControlFrameTooLarge { size } => {
65                write!(f, "control frame too large: {size} bytes (max 125)")
66            }
67            Self::FragmentedControlFrame => write!(f, "fragmented control frame"),
68            Self::InvalidCloseCode(code) => write!(f, "invalid close code: {code}"),
69            Self::InvalidUtf8InCloseReason => write!(f, "invalid UTF-8 in close reason"),
70            Self::CloseFrameTooShort => {
71                write!(f, "close frame too short (1 byte, must be 0 or >= 2)")
72            }
73            Self::ContinuationWithoutStart => {
74                write!(f, "continuation frame without preceding start frame")
75            }
76            Self::NewMessageDuringAssembly => {
77                write!(f, "new data frame received during fragment assembly")
78            }
79            Self::InvalidUtf8 => write!(f, "text message contains invalid UTF-8"),
80            Self::MessageTooLarge { accumulated, max } => {
81                write!(
82                    f,
83                    "assembled message too large: {accumulated} bytes (max {max})"
84                )
85            }
86        }
87    }
88}
89
90impl std::error::Error for ProtocolError {}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn display_invalid_opcode() {
98        let err = ProtocolError::InvalidOpcode(0x3);
99        assert_eq!(err.to_string(), "invalid opcode: 0x3");
100    }
101
102    #[test]
103    fn display_reserved_bits_set() {
104        let err = ProtocolError::ReservedBitsSet { bits: 0b110 };
105        assert_eq!(err.to_string(), "reserved bits set: 0b110");
106    }
107
108    #[test]
109    fn display_masked_frame_from_server() {
110        assert_eq!(
111            ProtocolError::MaskedFrameFromServer.to_string(),
112            "server sent masked frame"
113        );
114    }
115
116    #[test]
117    fn display_unmasked_frame_from_client() {
118        assert_eq!(
119            ProtocolError::UnmaskedFrameFromClient.to_string(),
120            "client sent unmasked frame"
121        );
122    }
123
124    #[test]
125    fn display_payload_too_large() {
126        let err = ProtocolError::PayloadTooLarge {
127            size: 200,
128            max: 125,
129        };
130        assert_eq!(err.to_string(), "payload too large: 200 bytes (max 125)");
131    }
132
133    #[test]
134    fn display_control_frame_too_large() {
135        let err = ProtocolError::ControlFrameTooLarge { size: 130 };
136        assert_eq!(
137            err.to_string(),
138            "control frame too large: 130 bytes (max 125)"
139        );
140    }
141
142    #[test]
143    fn display_fragmented_control_frame() {
144        assert_eq!(
145            ProtocolError::FragmentedControlFrame.to_string(),
146            "fragmented control frame"
147        );
148    }
149
150    #[test]
151    fn display_invalid_close_code() {
152        let err = ProtocolError::InvalidCloseCode(999);
153        assert_eq!(err.to_string(), "invalid close code: 999");
154    }
155
156    #[test]
157    fn display_invalid_utf8_in_close_reason() {
158        assert_eq!(
159            ProtocolError::InvalidUtf8InCloseReason.to_string(),
160            "invalid UTF-8 in close reason"
161        );
162    }
163
164    #[test]
165    fn display_close_frame_too_short() {
166        assert_eq!(
167            ProtocolError::CloseFrameTooShort.to_string(),
168            "close frame too short (1 byte, must be 0 or >= 2)"
169        );
170    }
171
172    #[test]
173    fn display_continuation_without_start() {
174        assert_eq!(
175            ProtocolError::ContinuationWithoutStart.to_string(),
176            "continuation frame without preceding start frame"
177        );
178    }
179
180    #[test]
181    fn display_new_message_during_assembly() {
182        assert_eq!(
183            ProtocolError::NewMessageDuringAssembly.to_string(),
184            "new data frame received during fragment assembly"
185        );
186    }
187
188    #[test]
189    fn display_invalid_utf8() {
190        assert_eq!(
191            ProtocolError::InvalidUtf8.to_string(),
192            "text message contains invalid UTF-8"
193        );
194    }
195
196    #[test]
197    fn display_message_too_large() {
198        let err = ProtocolError::MessageTooLarge {
199            accumulated: 2000,
200            max: 1024,
201        };
202        assert_eq!(
203            err.to_string(),
204            "assembled message too large: 2000 bytes (max 1024)"
205        );
206    }
207
208    #[test]
209    fn protocol_error_eq() {
210        assert_eq!(
211            ProtocolError::InvalidOpcode(0x3),
212            ProtocolError::InvalidOpcode(0x3)
213        );
214        assert_ne!(
215            ProtocolError::InvalidOpcode(0x3),
216            ProtocolError::InvalidOpcode(0x4)
217        );
218        assert_ne!(
219            ProtocolError::MaskedFrameFromServer,
220            ProtocolError::UnmaskedFrameFromClient
221        );
222    }
223}