Skip to main content

actix_http/ws/
frame.rs

1use std::{cmp::min, io, str};
2
3use bytes::{Buf, BufMut, BytesMut};
4
5use super::{
6    mask::apply_mask,
7    proto::{CloseCode, CloseReason, OpCode},
8    ProtocolError,
9};
10
11/// A struct representing a WebSocket frame.
12#[derive(Debug)]
13pub struct Parser;
14
15/// FIN bitmask (RFC 6455 §5.2).
16const FIN_MASK: u8 = 0x80;
17
18/// RSV1, RSV2, and RSV3 bitmask (RFC 6455 §5.2).
19const RSV_MASK: u8 = 0b0111_0000;
20
21/// OpCode 4-bit nibble mask (RFC 6455 §5.2).
22const OPCODE_MASK: u8 = 0x0F;
23
24/// MASK bit bitmask (RFC 6455 §5.2).
25const MASK_BIT: u8 = 0x80;
26
27/// 7-bit payload length bitmask (RFC 6455 §5.2).
28const PAYLOAD_LEN_MASK: u8 = 0x7F;
29
30/// Extended payload length indicator for 16-bit length (RFC 6455 §5.2).
31const EXT_LEN_U16: u8 = 126;
32
33/// Extended payload length indicator for 64-bit length (RFC 6455 §5.2).
34const EXT_LEN_U64: u8 = 127;
35
36/// Maximum payload length allowed for control frames (RFC 6455 §5.5).
37const MAX_CONTROL_FRAME_PAYLOAD: usize = 125;
38
39/// Maximum payload length representable by a 16-bit extended length (RFC 6455 §5.2).
40const U16_PAYLOAD_MAX: usize = u16::MAX as usize;
41
42/// Maximum legal payload length for a 64-bit extended length (RFC 6455 §5.2).
43const U64_PAYLOAD_MAX: u64 = i64::MAX as u64;
44
45/// Client masking key length in bytes (RFC 6455 §5.3).
46const MASK_LEN: usize = 4;
47
48/// Convert a payload length to `usize`, failing if it does not fit.
49///
50/// A `usize` cannot represent every 63-bit length on 32-bit targets, where the conversion must
51/// fail rather than truncate. Truncating would make the parser report a shorter frame and treat
52/// the remaining bytes as the start of the next one.
53fn payload_len_to_usize(len: u64) -> Result<usize, ProtocolError> {
54    usize::try_from(len).map_err(|_| ProtocolError::Overflow)
55}
56
57/// Whether the most significant bit of a 64-bit payload length is set.
58///
59/// RFC 6455 §5.2 defines the payload length as an unsigned 63-bit integer, so the most
60/// significant bit must be zero.
61fn payload_len_msb_set(len: u64) -> bool {
62    len > U64_PAYLOAD_MAX
63}
64
65impl Parser {
66    fn parse_metadata(
67        src: &[u8],
68        server: bool,
69    ) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
70        let chunk_len = src.len();
71
72        let mut idx = 2;
73        if chunk_len < 2 {
74            return Ok(None);
75        }
76
77        let first = src[0];
78        let second = src[1];
79        let finished = first & FIN_MASK != 0;
80
81        // RSV1, RSV2, and RSV3 must be zero unless a negotiated extension defines them.
82        if first & RSV_MASK != 0 {
83            // TODO(semver-major): use InvalidReservedBits
84            return Err(ProtocolError::Io(io::Error::new(
85                io::ErrorKind::InvalidData,
86                "Received a frame with non-zero reserved bits",
87            )));
88        }
89
90        // check masking
91        let masked = second & MASK_BIT != 0;
92        if !masked && server {
93            return Err(ProtocolError::UnmaskedFrame);
94        } else if masked && !server {
95            return Err(ProtocolError::MaskedFrame);
96        }
97
98        // Op code
99        let opcode = OpCode::from(first & OPCODE_MASK);
100
101        if let OpCode::Bad = opcode {
102            return Err(ProtocolError::InvalidOpcode(first & OPCODE_MASK));
103        }
104
105        let len = second & PAYLOAD_LEN_MASK;
106        let length = if len == EXT_LEN_U16 {
107            if chunk_len < 4 {
108                return Ok(None);
109            }
110            let len = usize::from(u16::from_be_bytes(
111                TryFrom::try_from(&src[idx..idx + 2]).unwrap(),
112            ));
113            idx += 2;
114            len
115        } else if len == EXT_LEN_U64 {
116            if chunk_len < 10 {
117                return Ok(None);
118            }
119            let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
120            idx += 8;
121
122            if payload_len_msb_set(len) {
123                // TODO(semver-major): use a variant that describes an invalid length, rather than
124                //  the payload size limit that `Overflow` also documents.
125                return Err(ProtocolError::Overflow);
126            }
127
128            payload_len_to_usize(len)?
129        } else {
130            len as usize
131        };
132
133        let mask = if server {
134            if chunk_len < idx + MASK_LEN {
135                return Ok(None);
136            }
137
138            let mask = TryFrom::try_from(&src[idx..idx + MASK_LEN]).unwrap();
139
140            idx += MASK_LEN;
141
142            Some(mask)
143        } else {
144            None
145        };
146
147        Ok(Some((idx, finished, opcode, length, mask)))
148    }
149
150    /// Parse the input stream into a frame.
151    pub fn parse(
152        src: &mut BytesMut,
153        server: bool,
154        max_size: usize,
155    ) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
156        // try to parse ws frame metadata
157        let (idx, finished, opcode, length, mask) = match Parser::parse_metadata(src, server)? {
158            None => return Ok(None),
159            Some(res) => res,
160        };
161
162        // Live on 32-bit targets, where `u32::MAX` fits a `usize` and overflows this. On 64-bit
163        // it cannot fire: overflowing needs a length above `u64::MAX - idx`, which has the most
164        // significant bit set and is rejected earlier.
165        let frame_len = match idx.checked_add(length) {
166            Some(len) => len,
167            None => return Err(ProtocolError::Overflow),
168        };
169
170        // not enough data
171        if src.len() < frame_len {
172            let min_length = min(length, max_size);
173            let required_cap = match idx.checked_add(min_length) {
174                Some(cap) => cap,
175                None => return Err(ProtocolError::Overflow),
176            };
177
178            if src.capacity() < required_cap {
179                src.reserve(required_cap - src.capacity());
180            }
181            return Ok(None);
182        }
183
184        // remove prefix
185        src.advance(idx);
186
187        // check for max allowed size
188        if length > max_size {
189            // drop the payload
190            src.advance(length);
191            return Err(ProtocolError::Overflow);
192        }
193
194        // no need for body
195        if length == 0 {
196            return Ok(Some((finished, opcode, None)));
197        }
198
199        let mut data = src.split_to(length);
200
201        // control frames must have length <= 125 (RFC 6455 §5.5)
202        match opcode {
203            OpCode::Ping | OpCode::Pong | OpCode::Close if length > MAX_CONTROL_FRAME_PAYLOAD => {
204                return Err(ProtocolError::InvalidLength(length));
205            }
206            _ => {}
207        }
208
209        // unmask
210        if let Some(mask) = mask {
211            apply_mask(&mut data, mask);
212        }
213
214        Ok(Some((finished, opcode, Some(data))))
215    }
216
217    /// Parse the payload of a close frame.
218    ///
219    /// This method preserves the historical behavior of accepting unknown status codes and
220    /// replacing invalid UTF-8 in the reason. Use [`Parser::try_parse_close_payload`] when the
221    /// payload must be validated.
222    #[deprecated(
223        since = "3.13.5",
224        note = "Use `Parser::try_parse_close_payload` instead."
225    )]
226    pub fn parse_close_payload(payload: &[u8]) -> Option<CloseReason> {
227        if payload.len() >= 2 {
228            let raw_code = u16::from_be_bytes(TryFrom::try_from(&payload[..2]).unwrap());
229            let code = CloseCode::from(raw_code);
230            let description = if payload.len() > 2 {
231                Some(String::from_utf8_lossy(&payload[2..]).into())
232            } else {
233                None
234            };
235            Some(CloseReason { code, description })
236        } else {
237            None
238        }
239    }
240
241    /// Parse and validate the payload of a close frame.
242    ///
243    /// # Validation
244    ///
245    /// A close payload is either empty or contains a two-byte status code followed by an optional
246    /// UTF-8 reason. This follows [RFC 6455 §5.5.1], with status code rules from [RFC 6455 §7.4]
247    /// and UTF-8 error handling from [RFC 6455 §8.1].
248    ///
249    /// [RFC 6455 §5.5.1]: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1
250    /// [RFC 6455 §7.4]: https://datatracker.ietf.org/doc/html/rfc6455#section-7.4
251    /// [RFC 6455 §8.1]: https://datatracker.ietf.org/doc/html/rfc6455#section-8.1
252    pub fn try_parse_close_payload(payload: &[u8]) -> Result<Option<CloseReason>, ProtocolError> {
253        // RFC 6455 §5.5.1 requires a two-byte status code when a close payload is not empty.
254        if payload.len() == 1 {
255            return Err(ProtocolError::InvalidLength(payload.len()));
256        }
257
258        if payload.len() >= 2 {
259            let raw_code = u16::from_be_bytes(
260                payload[..2]
261                    .try_into()
262                    .expect("Payload length should be checked before parsing"),
263            );
264
265            // RFC 6455 §7.4 reserves 1000-2999 for protocol-defined codes. Reject reserved and
266            // undefined values while allowing registered codes (3000-3999) and private-use codes
267            // (4000-4999).
268            if !matches!(raw_code, 1000..=1003 | 1007..=1014 | 3000..=4999) {
269                // TODO(semver-major): use this instead
270                // return Err(ProtocolError::InvalidCloseCode(raw_code));
271                return Err(ProtocolError::BadOpCode);
272            }
273
274            if payload.len() > 2 {
275                // RFC 6455 §5.5.1 defines the remaining bytes as a UTF-8 reason. RFC 6455 §8.1
276                // requires the connection to fail when data interpreted as UTF-8 is invalid.
277                str::from_utf8(&payload[2..]).map_err(|_| {
278                    // TODO(semver-major): use this instead
279                    // ProtocolError::InvalidCloseReason
280                    ProtocolError::BadOpCode
281                })?;
282            }
283        }
284
285        #[expect(deprecated)]
286        Ok(Self::parse_close_payload(payload))
287    }
288
289    /// Generate binary representation
290    pub fn write_message<B: AsRef<[u8]>>(
291        dst: &mut BytesMut,
292        pl: B,
293        op: OpCode,
294        fin: bool,
295        mask: bool,
296    ) {
297        let payload = pl.as_ref();
298        let one = if fin {
299            FIN_MASK | u8::from(op)
300        } else {
301            u8::from(op)
302        };
303        let payload_len = payload.len();
304        let (two, p_len) = if mask {
305            (MASK_BIT, payload_len + MASK_LEN)
306        } else {
307            (0, payload_len)
308        };
309
310        if payload_len < EXT_LEN_U16 as usize {
311            dst.reserve(p_len + 2);
312            dst.put_slice(&[one, two | payload_len as u8]);
313        } else if payload_len <= U16_PAYLOAD_MAX {
314            dst.reserve(p_len + 4);
315            dst.put_slice(&[one, two | EXT_LEN_U16]);
316            dst.put_u16(payload_len as u16);
317        } else {
318            dst.reserve(p_len + 10);
319            dst.put_slice(&[one, two | EXT_LEN_U64]);
320            dst.put_u64(payload_len as u64);
321        };
322
323        if mask {
324            let mask = rand::random::<[u8; 4]>();
325            dst.put_slice(mask.as_ref());
326            dst.put_slice(payload.as_ref());
327            let pos = dst.len() - payload_len;
328            apply_mask(&mut dst[pos..], mask);
329        } else {
330            dst.put_slice(payload.as_ref());
331        }
332    }
333
334    /// Create a new Close control frame.
335    #[inline]
336    pub fn write_close(dst: &mut BytesMut, reason: Option<CloseReason>, mask: bool) {
337        let payload = match reason {
338            None => Vec::new(),
339            Some(reason) => {
340                let mut payload = Into::<u16>::into(reason.code).to_be_bytes().to_vec();
341                if let Some(description) = reason.description {
342                    payload.extend(description.as_bytes());
343                }
344                payload
345            }
346        };
347
348        Parser::write_message(dst, payload, OpCode::Close, true, mask)
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use bytes::Bytes;
355
356    use super::*;
357
358    struct F {
359        finished: bool,
360        opcode: OpCode,
361        payload: Bytes,
362    }
363
364    fn is_none(frm: &Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError>) -> bool {
365        matches!(*frm, Ok(None))
366    }
367
368    fn extract(frm: Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError>) -> F {
369        match frm {
370            Ok(Some((finished, opcode, payload))) => F {
371                finished,
372                opcode,
373                payload: payload
374                    .map(|b| b.freeze())
375                    .unwrap_or_else(|| Bytes::from("")),
376            },
377            _ => unreachable!("error"),
378        }
379    }
380
381    // RFC 6455 §5.2: the most significant bit of a 64-bit payload length MUST be 0.
382    #[test]
383    fn test_parse_rejects_64_bit_length_with_msb_set() {
384        let mut buf = BytesMut::from(&[0x81u8, 127][..]);
385        buf.extend_from_slice(&(U64_PAYLOAD_MAX + 1).to_be_bytes());
386
387        assert!(
388            matches!(
389                Parser::parse(&mut buf, false, 1024),
390                Err(ProtocolError::Overflow)
391            ),
392            "a 64-bit payload length with the MSB set must be rejected, not treated as incomplete"
393        );
394        // The rejection must not consume the header, or the next parse would start mid-frame.
395        assert_eq!(buf.len(), 10);
396    }
397
398    // Pins the accept side of the boundary through `parse`: the largest legal length must be
399    // treated as incomplete, not rejected. Only a 64-bit `usize` can represent it.
400    #[cfg(target_pointer_width = "64")]
401    #[test]
402    fn test_parse_accepts_largest_legal_64_bit_length() {
403        let mut buf = BytesMut::from(&[0x81u8, 127][..]);
404        buf.extend_from_slice(&U64_PAYLOAD_MAX.to_be_bytes());
405
406        // `max_size` is deliberately far below the declared length: an incomplete frame returns
407        // before the size check, so the header is accepted as incomplete. It also keeps the
408        // parser from reserving the declared length, which a large `max_size` would attempt.
409        assert!(
410            is_none(&Parser::parse(&mut buf, false, 1024)),
411            "the largest legal 64-bit length must be incomplete, not rejected"
412        );
413        // The header must stay buffered while the payload is awaited.
414        assert_eq!(buf.len(), 10);
415    }
416
417    // The most-significant-bit rule itself, on every target. On 32-bit the conversion rejects every
418    // such length anyway, so the call site is not observable through `parse`; pin the predicate.
419    #[test]
420    fn test_payload_len_msb_set() {
421        assert!(!payload_len_msb_set(0));
422        assert!(!payload_len_msb_set(U64_PAYLOAD_MAX));
423        assert!(payload_len_msb_set(1 << 63));
424        assert!(payload_len_msb_set(u64::MAX));
425    }
426
427    // A declared 2^32 does not fit a 32-bit `usize`, and truncating it would accept an empty frame
428    // and leave the trailing bytes to be read as the next frame.
429    #[cfg(target_pointer_width = "32")]
430    #[test]
431    fn test_parse_rejects_64_bit_length_beyond_addressable_range() {
432        let mut buf = BytesMut::from(&[0x81u8, 127][..]);
433        buf.extend_from_slice(&0x0000_0001_0000_0000u64.to_be_bytes());
434        buf.extend_from_slice(b"ABCD");
435
436        assert!(
437            matches!(
438                Parser::parse(&mut buf, false, 1024),
439                Err(ProtocolError::Overflow)
440            ),
441            "a length that does not fit in usize must be rejected, not truncated to 0"
442        );
443        assert_eq!(buf.len(), 14);
444    }
445
446    // `u32::MAX` fits a 32-bit `usize` and its most significant bit is clear, so neither the
447    // most-significant-bit check nor the conversion rejects it and only the `checked_add` guard in
448    // `parse` can.
449    #[cfg(target_pointer_width = "32")]
450    #[test]
451    fn test_parse_rejects_length_that_overflows_frame_len() {
452        let mut buf = BytesMut::from(&[0x81u8, 127][..]);
453        buf.extend_from_slice(&(u32::MAX as u64).to_be_bytes());
454
455        assert!(matches!(
456            Parser::parse(&mut buf, false, 1024),
457            Err(ProtocolError::Overflow)
458        ));
459    }
460
461    #[test]
462    fn test_parse() {
463        let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
464        assert!(is_none(&Parser::parse(&mut buf, false, 1024)));
465
466        let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
467        buf.extend(b"1");
468
469        let frame = extract(Parser::parse(&mut buf, false, 1024));
470        assert!(!frame.finished);
471        assert_eq!(frame.opcode, OpCode::Text);
472        assert_eq!(frame.payload.as_ref(), &b"1"[..]);
473    }
474
475    #[test]
476    fn test_parse_length0() {
477        let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0000u8][..]);
478        let frame = extract(Parser::parse(&mut buf, false, 1024));
479        assert!(!frame.finished);
480        assert_eq!(frame.opcode, OpCode::Text);
481        assert!(frame.payload.is_empty());
482    }
483
484    #[test]
485    fn test_parse_length2() {
486        let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]);
487        assert!(is_none(&Parser::parse(&mut buf, false, 1024)));
488
489        let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]);
490        buf.extend(&[0u8, 4u8][..]);
491        buf.extend(b"1234");
492
493        let frame = extract(Parser::parse(&mut buf, false, 1024));
494        assert!(!frame.finished);
495        assert_eq!(frame.opcode, OpCode::Text);
496        assert_eq!(frame.payload.as_ref(), &b"1234"[..]);
497    }
498
499    #[test]
500    fn test_parse_length4() {
501        let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]);
502        assert!(is_none(&Parser::parse(&mut buf, false, 1024)));
503
504        let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]);
505        buf.extend(&[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 4u8][..]);
506        buf.extend(b"1234");
507
508        let frame = extract(Parser::parse(&mut buf, false, 1024));
509        assert!(!frame.finished);
510        assert_eq!(frame.opcode, OpCode::Text);
511        assert_eq!(frame.payload.as_ref(), &b"1234"[..]);
512    }
513
514    #[test]
515    fn test_parse_frame_mask() {
516        let mut buf = BytesMut::from(&[0b0000_0001u8, 0b1000_0001u8][..]);
517        buf.extend(b"0001");
518        buf.extend(b"1");
519
520        assert!(Parser::parse(&mut buf, false, 1024).is_err());
521
522        let frame = extract(Parser::parse(&mut buf, true, 1024));
523        assert!(!frame.finished);
524        assert_eq!(frame.opcode, OpCode::Text);
525        assert_eq!(frame.payload, Bytes::from(vec![1u8]));
526    }
527
528    #[test]
529    fn test_parse_frame_no_mask() {
530        let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
531        buf.extend([1u8]);
532
533        assert!(Parser::parse(&mut buf, true, 1024).is_err());
534
535        let frame = extract(Parser::parse(&mut buf, false, 1024));
536        assert!(!frame.finished);
537        assert_eq!(frame.opcode, OpCode::Text);
538        assert_eq!(frame.payload, Bytes::from(vec![1u8]));
539    }
540
541    #[test]
542    fn test_parse_frame_with_rsv1_set() {
543        // https://github.com/actix/actix-web/issues/1579
544
545        // Final, masked text frame with RSV1 set and no negotiated extension.
546        let mut buf = BytesMut::from(
547            &[
548                0b1100_0001u8, // FIN + RSV1 + text
549                0b1000_0001u8, // MASK + payload length
550                0,             // masking key byte 1
551                0,             // masking key byte 2
552                0,             // masking key byte 3
553                0,             // masking key byte 4
554                b'a',          // payload
555            ][..],
556        );
557
558        Parser::parse(&mut buf, true, 1024)
559            .expect_err("Should reject set RSV1 bit when no extension is negotiated");
560    }
561
562    #[test]
563    fn test_parse_frame_max_size() {
564        let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]);
565        buf.extend([1u8, 1u8]);
566
567        assert!(Parser::parse(&mut buf, true, 1).is_err());
568
569        if let Err(ProtocolError::Overflow) = Parser::parse(&mut buf, false, 0) {
570        } else {
571            unreachable!("error");
572        }
573    }
574
575    #[test]
576    fn test_parse_frame_max_size_recoverability() {
577        let mut buf = BytesMut::new();
578        // The first text frame with length == 2, payload doesn't matter.
579        buf.extend([0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
580        // Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
581        buf.extend([0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
582
583        assert_eq!(buf.len(), 8);
584        assert!(matches!(
585            Parser::parse(&mut buf, false, 1),
586            Err(ProtocolError::Overflow)
587        ));
588        assert_eq!(buf.len(), 4);
589        let frame = extract(Parser::parse(&mut buf, false, 2));
590        assert!(!frame.finished);
591        assert_eq!(frame.opcode, OpCode::Binary);
592        assert_eq!(
593            frame.payload,
594            Bytes::from(vec![0b1111_1111u8, 0b1111_1111u8])
595        );
596        assert_eq!(buf.len(), 0);
597    }
598
599    #[test]
600    fn test_ping_frame() {
601        let mut buf = BytesMut::new();
602        Parser::write_message(&mut buf, Vec::from("data"), OpCode::Ping, true, false);
603
604        let mut v = vec![137u8, 4u8];
605        v.extend(b"data");
606        assert_eq!(&buf[..], &v[..]);
607    }
608
609    #[test]
610    fn test_pong_frame() {
611        let mut buf = BytesMut::new();
612        Parser::write_message(&mut buf, Vec::from("data"), OpCode::Pong, true, false);
613
614        let mut v = vec![138u8, 4u8];
615        v.extend(b"data");
616        assert_eq!(&buf[..], &v[..]);
617    }
618
619    #[test]
620    fn test_close_frame() {
621        let mut buf = BytesMut::new();
622        let reason = (CloseCode::Normal, "data");
623        Parser::write_close(&mut buf, Some(reason.into()), false);
624
625        let mut v = vec![136u8, 6u8, 3u8, 232u8];
626        v.extend(b"data");
627        assert_eq!(&buf[..], &v[..]);
628    }
629
630    #[test]
631    fn test_empty_close_frame() {
632        let mut buf = BytesMut::new();
633        Parser::write_close(&mut buf, None, false);
634        assert_eq!(&buf[..], &vec![0x88, 0x00][..]);
635    }
636
637    #[test]
638    fn try_parse_close_payload_validates_payload() {
639        assert!(matches!(
640            Parser::try_parse_close_payload(&[0x03, 0xe8, 0xff]).unwrap_err(),
641            ProtocolError::BadOpCode
642        ));
643        assert!(matches!(
644            Parser::try_parse_close_payload(&[0, 0]).unwrap_err(),
645            ProtocolError::BadOpCode
646        ));
647    }
648
649    #[test]
650    fn test_oversized_control_frames() {
651        // Ping frame with 126 bytes payload (exceeding 125 limit)
652        let mut buf = BytesMut::from(&[0x89u8, 126u8, 0x00, 126][..]);
653        buf.extend(vec![0u8; 126]);
654        assert!(matches!(
655            Parser::parse(&mut buf, false, 65536),
656            Err(ProtocolError::InvalidLength(126))
657        ));
658
659        // Close frame with 126 bytes payload (exceeding 125 limit)
660        let mut buf = BytesMut::from(&[0x88u8, 126u8, 0x00, 126][..]);
661        buf.extend(vec![0u8; 126]);
662        assert!(matches!(
663            Parser::parse(&mut buf, false, 65536),
664            Err(ProtocolError::InvalidLength(126))
665        ));
666    }
667
668    // The payload length of the #3723 repro, whose MSB is set. That report was an unchecked
669    // addition that overflowed; `checked_add` was added to fix it, and the most-significant-bit
670    // check now rejects this input before either runs, so this pins the regression only.
671    // The `checked_add` guard is covered separately by a 32-bit-only test.
672    #[test]
673    fn test_parse_rejects_msb_length_from_3723() {
674        let buf: [u8; 14] = [
675            0x0a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xeb, 0x0e, 0x8f,
676        ];
677        let mut buf = BytesMut::from(&buf[..]);
678        let result = Parser::parse(&mut buf, true, 65536);
679        // Opcode 0x0a is Pong. The length is rejected in `parse_metadata` before the frame can be
680        // buffered, so the header is reported as malformed rather than awaited.
681        assert!(
682            matches!(result, Err(ProtocolError::Overflow)),
683            "a declared length above i64::MAX must be rejected outright"
684        );
685    }
686}