Skip to main content

thunder/wire/
frame.rs

1//! Length-prefixed MessagePack frame codec.
2//!
3//! ```text
4//! ┌───────────────────┬──────────────────────────┐
5//! │  length: u32 (LE) │  body: MessagePack bytes │
6//! └───────────────────┴──────────────────────────┘
7//!     4 bytes              length bytes
8//! ```
9//!
10//! The cap is validated against the length prefix **before** the body
11//! buffer is allocated (WIRE-020/021), so a hostile prefix cannot exhaust
12//! memory. Decode reports how many bytes one frame consumed, which is also
13//! the frame size consumers feed to metrics — nothing ever re-encodes a
14//! frame just to measure it (SRV-007).
15
16use serde::{Deserialize, Serialize};
17
18use crate::wire::DEFAULT_MAX_FRAME_BYTES;
19
20/// Errors from the sync decoder.
21#[derive(Debug, thiserror::Error)]
22pub enum DecodeError {
23    /// The length prefix declared a body larger than the caller's cap.
24    /// Raised before any body allocation (WIRE-021).
25    #[error("frame body {body} bytes exceeds limit {max} bytes")]
26    FrameTooLarge { body: usize, max: usize },
27    /// Well-formed frame, malformed MessagePack payload (WIRE-023).
28    #[error("decode error: {0}")]
29    Rmp(#[from] rmp_serde::decode::Error),
30    /// A zero-length frame: a valid **keep-alive** carrying no body
31    /// (WIRE-024), reached through a typed decode that needs one.
32    ///
33    /// Distinct from [`DecodeError::Rmp`] on purpose. A zero-length body
34    /// genuinely cannot be a `Request`/`Response`, but it is not malformed —
35    /// it is a liveness tick a peer deliberately sent. Callers match on the
36    /// intent and skip it; before this variant existed they had to recognize
37    /// it as a parse failure, which is why products wrapped the reader to eat
38    /// zero-length frames before delegating.
39    ///
40    /// [`decode_frame_raw`] returns these as an empty body instead.
41    #[error("zero-length frame (keep-alive), not a message body")]
42    KeepAlive,
43}
44
45/// Encode a message into one complete frame (`u32 LE length` + body).
46pub fn encode_frame<T: Serialize>(msg: &T) -> Result<Vec<u8>, rmp_serde::encode::Error> {
47    let body = rmp_serde::to_vec(msg)?;
48    let len = body.len() as u32;
49    let mut frame = Vec::with_capacity(4 + body.len());
50    frame.extend_from_slice(&len.to_le_bytes());
51    frame.extend_from_slice(&body);
52    Ok(frame)
53}
54
55/// Decode one frame from a byte slice using [`DEFAULT_MAX_FRAME_BYTES`].
56///
57/// Returns `Ok(None)` when the buffer does not yet hold a complete frame
58/// (read more and retry — WIRE-022). On success returns the value and the
59/// total bytes consumed (`4 + body`), which is the frame size for metrics.
60pub fn decode_frame<T: for<'de> Deserialize<'de>>(
61    buf: &[u8],
62) -> Result<Option<(T, usize)>, DecodeError> {
63    decode_frame_with_limit(buf, DEFAULT_MAX_FRAME_BYTES)
64}
65
66/// Decode one frame's **body, borrowed from `buf`**, plus the total bytes
67/// consumed (`4 + body`).
68///
69/// This is the framing layer on its own: the prefix read, the cap check
70/// (WIRE-020/021, before any slicing) and the body slice — with no assumption
71/// about what the body *is*. A product whose frames carry its own envelope
72/// rather than Thunder's `Request`/`Response` reuses the framing through this
73/// instead of reimplementing it, which is the whole point of a shared wire
74/// crate. It is the Rust counterpart of the TypeScript package's
75/// `FrameReader`.
76///
77/// Returns `Ok(None)` when the buffer does not yet hold a complete frame
78/// (read more and retry — WIRE-022).
79///
80/// A zero-length frame is **not** an error here: it comes back as an empty
81/// body slice, which is what lets a raw consumer skip a keep-alive
82/// (WIRE-024).
83///
84/// [`decode_frame_with_limit`] is this function plus a MessagePack decode, so
85/// there is exactly one implementation of the framing rules.
86pub fn decode_frame_raw(buf: &[u8], max: usize) -> Result<Option<(&[u8], usize)>, DecodeError> {
87    if buf.len() < 4 {
88        return Ok(None);
89    }
90    let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
91    // WIRE-021: judged against the prefix, before the body is sliced or
92    // allocated.
93    if len > max {
94        return Err(DecodeError::FrameTooLarge { body: len, max });
95    }
96    let total = 4 + len;
97    if buf.len() < total {
98        return Ok(None);
99    }
100    Ok(Some((&buf[4..total], total)))
101}
102
103/// Decode one frame, rejecting bodies larger than `max` before the body
104/// is even inspected (WIRE-020/021).
105///
106/// A zero-length frame yields [`DecodeError::KeepAlive`]: it is a valid frame
107/// (WIRE-024) but carries no body to deserialize. Use [`decode_frame_raw`] to
108/// consume it as an empty body instead.
109pub fn decode_frame_with_limit<T: for<'de> Deserialize<'de>>(
110    buf: &[u8],
111    max: usize,
112) -> Result<Option<(T, usize)>, DecodeError> {
113    let Some((body, total)) = decode_frame_raw(buf, max)? else {
114        return Ok(None);
115    };
116    if body.is_empty() {
117        return Err(DecodeError::KeepAlive);
118    }
119    let value = rmp_serde::from_slice(body)?;
120    Ok(Some((value, total)))
121}
122
123// ── Async helpers (feature = "tokio") ───────────────────────────────────────
124
125#[cfg(feature = "tokio")]
126mod tokio_io {
127    use std::io;
128
129    use serde::{Deserialize, Serialize};
130    use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
131
132    use crate::wire::value::{Request, Response};
133    use crate::wire::DEFAULT_MAX_FRAME_BYTES;
134
135    /// Read one frame; returns the decoded value and the frame size in
136    /// bytes (`4 + body` — the metrics input, SRV-007). The cap is checked
137    /// between reading the prefix and allocating the body (WIRE-020).
138    pub async fn read_frame<T: for<'de> Deserialize<'de>, R: AsyncRead + Unpin>(
139        reader: &mut R,
140        max: usize,
141    ) -> io::Result<(T, usize)> {
142        let mut len_buf = [0u8; 4];
143        reader.read_exact(&mut len_buf).await?;
144        let len = u32::from_le_bytes(len_buf) as usize;
145        if len > max {
146            return Err(io::Error::new(
147                io::ErrorKind::InvalidData,
148                format!("frame body {len} bytes exceeds limit {max} bytes"),
149            ));
150        }
151        // WIRE-024: a zero-length frame is a valid keep-alive, but this helper
152        // reads a *typed* body, so it reports the intent rather than a
153        // MessagePack parse failure — the sync path's DecodeError::KeepAlive,
154        // spelled for io::Error.
155        //
156        // Note this does not decide what a Thunder *server* does with a
157        // keep-alive: the read loop still treats the error as a failed read.
158        // Making the listener skip them is a separate behavioural choice and
159        // is deliberately not made here.
160        if len == 0 {
161            return Err(io::Error::new(
162                io::ErrorKind::InvalidData,
163                "zero-length frame (keep-alive), not a message body",
164            ));
165        }
166        let mut body = vec![0u8; len];
167        reader.read_exact(&mut body).await?;
168        let value = rmp_serde::from_slice(&body)
169            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
170        Ok((value, 4 + len))
171    }
172
173    /// Read one [`Request`] with the default cap.
174    pub async fn read_request<R: AsyncRead + Unpin>(
175        reader: &mut R,
176    ) -> io::Result<(Request, usize)> {
177        read_frame(reader, DEFAULT_MAX_FRAME_BYTES).await
178    }
179
180    /// Read one [`Request`] with a caller-supplied cap (server hot path).
181    pub async fn read_request_with_limit<R: AsyncRead + Unpin>(
182        reader: &mut R,
183        max: usize,
184    ) -> io::Result<(Request, usize)> {
185        read_frame(reader, max).await
186    }
187
188    /// Read one [`Response`] with the default cap.
189    pub async fn read_response<R: AsyncRead + Unpin>(
190        reader: &mut R,
191    ) -> io::Result<(Response, usize)> {
192        read_frame(reader, DEFAULT_MAX_FRAME_BYTES).await
193    }
194
195    /// Read one [`Response`] with a caller-supplied cap.
196    pub async fn read_response_with_limit<R: AsyncRead + Unpin>(
197        reader: &mut R,
198        max: usize,
199    ) -> io::Result<(Response, usize)> {
200        read_frame(reader, max).await
201    }
202
203    /// Encode and write one frame; returns the frame size written.
204    pub async fn write_frame<T: Serialize, W: AsyncWrite + Unpin>(
205        writer: &mut W,
206        msg: &T,
207    ) -> io::Result<usize> {
208        let frame = crate::wire::frame::encode_frame(msg)
209            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
210        writer.write_all(&frame).await?;
211        Ok(frame.len())
212    }
213
214    /// Write one [`Request`] frame.
215    pub async fn write_request<W: AsyncWrite + Unpin>(
216        writer: &mut W,
217        req: &Request,
218    ) -> io::Result<usize> {
219        write_frame(writer, req).await
220    }
221
222    /// Write one [`Response`] frame.
223    pub async fn write_response<W: AsyncWrite + Unpin>(
224        writer: &mut W,
225        resp: &Response,
226    ) -> io::Result<usize> {
227        write_frame(writer, resp).await
228    }
229}
230
231#[cfg(feature = "tokio")]
232pub use tokio_io::{
233    read_frame, read_request, read_request_with_limit, read_response, read_response_with_limit,
234    write_frame, write_request, write_response,
235};
236
237#[cfg(test)]
238#[allow(clippy::unwrap_used, clippy::expect_used)]
239mod tests {
240    use super::*;
241    use crate::wire::value::{Request, Response, Value};
242    use crate::wire::PUSH_ID;
243
244    fn hex(bytes: &[u8]) -> String {
245        bytes
246            .iter()
247            .map(|b| format!("{b:02x}"))
248            .collect::<Vec<_>>()
249            .join(" ")
250    }
251
252    // ── Golden vectors (family-pinned bytes, corpus canonical group) ───────
253
254    #[test]
255    fn ping_request_matches_family_golden_vector() {
256        let req = Request {
257            id: 1,
258            command: "PING".to_owned(),
259            args: vec![],
260        };
261        let frame = encode_frame(&req).unwrap();
262        assert_eq!(
263            hex(&frame),
264            "08 00 00 00 93 01 a4 50 49 4e 47 90",
265            "frame must match the corpus request-ping vector"
266        );
267        let (decoded, consumed): (Request, usize) = decode_frame(&frame).unwrap().unwrap();
268        assert_eq!(decoded, req);
269        assert_eq!(consumed, frame.len());
270    }
271
272    #[test]
273    fn pong_response_matches_nested_ok_golden_vector() {
274        let resp = Response::ok(1, Value::Str("PONG".to_owned()));
275        let frame = encode_frame(&resp).unwrap();
276        // Result<Value, String> nests two one-key maps: {"Ok": {"Str": "PONG"}}.
277        assert_eq!(
278            hex(&frame),
279            "10 00 00 00 92 01 81 a2 4f 6b 81 a3 53 74 72 a4 50 4f 4e 47"
280        );
281        let (decoded, _): (Response, usize) = decode_frame(&frame).unwrap().unwrap();
282        assert_eq!(decoded, resp);
283    }
284
285    #[test]
286    fn null_is_bare_string_and_int_is_single_key_map() {
287        assert_eq!(
288            hex(&rmp_serde::to_vec(&Value::Null).unwrap()),
289            "a4 4e 75 6c 6c"
290        );
291        assert_eq!(
292            hex(&rmp_serde::to_vec(&Value::Int(42)).unwrap()),
293            "81 a3 49 6e 74 2a"
294        );
295    }
296
297    // ── Bytes canonicalization (WIRE-010/011, probe T-029) ────────────────
298
299    #[test]
300    fn bytes_emit_as_bin_canonical() {
301        let encoded = rmp_serde::to_vec(&Value::bytes(vec![1, 2, 3, 255])).unwrap();
302        // {"Bytes": bin8(4)} — c4 04, never the int-array form (94 ... cc ff).
303        assert_eq!(hex(&encoded), "81 a5 42 79 74 65 73 c4 04 01 02 03 ff");
304    }
305
306    #[test]
307    fn bytes_decode_legacy_int_array_form() {
308        // The seq-of-u8 form every pre-Thunder Rust implementation emits.
309        let legacy: Vec<u8> = vec![
310            0x81, 0xa5, 0x42, 0x79, 0x74, 0x65, 0x73, // {"Bytes":
311            0x94, 0x01, 0x02, 0x03, 0xcc, 0xff, // [1, 2, 3, 255] as ints
312        ];
313        let decoded: Value = rmp_serde::from_slice(&legacy).unwrap();
314        assert_eq!(decoded, Value::bytes(vec![1, 2, 3, 255]));
315    }
316
317    // ── Request shape tolerance (WIRE-012/013) ────────────────────────────
318
319    #[test]
320    fn map_shaped_request_decodes() {
321        // Some pre-v1 encoders emit Request as a named map.
322        let req = Request {
323            id: 7,
324            command: "GET".to_owned(),
325            args: vec![Value::Str("key".to_owned())],
326        };
327        let map_shaped = rmp_serde::to_vec_named(&req).unwrap();
328        let array_shaped = rmp_serde::to_vec(&req).unwrap();
329        assert_ne!(map_shaped, array_shaped, "shapes must actually differ");
330        let decoded: Request = rmp_serde::from_slice(&map_shaped).unwrap();
331        assert_eq!(decoded, req);
332    }
333
334    // ── Round-trip matrix (donor test suite, WIRE-002/014/015) ────────────
335
336    #[test]
337    fn round_trip_all_variants() {
338        let all = Value::Array(vec![
339            Value::Null,
340            Value::Bool(true),
341            Value::Bool(false),
342            Value::Int(0),
343            Value::Int(i64::MIN),
344            Value::Int(i64::MAX),
345            Value::Int(-32),
346            Value::Int(127),
347            Value::Int(255),
348            Value::Int(65535),
349            Value::Float(0.0),
350            Value::Float(-0.0),
351            Value::Float(f64::INFINITY),
352            Value::Float(f64::NEG_INFINITY),
353            Value::bytes(vec![]),
354            Value::bytes(vec![0, 1, 2, 255]),
355            Value::Str(String::new()),
356            Value::Str("héllo wörld".to_owned()),
357            Value::Array(vec![]),
358            Value::Map(vec![]),
359            Value::Map(vec![
360                (Value::Str("k".to_owned()), Value::Int(1)),
361                (Value::Int(2), Value::Str("non-string key".to_owned())),
362            ]),
363        ]);
364        let frame = encode_frame(&all).unwrap();
365        let (decoded, consumed): (Value, usize) = decode_frame(&frame).unwrap().unwrap();
366        assert_eq!(decoded, all);
367        assert_eq!(consumed, frame.len());
368    }
369
370    #[test]
371    fn nan_bit_pattern_survives() {
372        let frame = encode_frame(&Value::Float(f64::NAN)).unwrap();
373        let (decoded, _): (Value, usize) = decode_frame(&frame).unwrap().unwrap();
374        match decoded {
375            Value::Float(f) => assert_eq!(f.to_bits(), f64::NAN.to_bits()),
376            other => panic!("expected Float, got {other:?}"),
377        }
378    }
379
380    #[test]
381    fn error_response_round_trips_with_prefix_conventions() {
382        for msg in [
383            "ERR unknown command",
384            "NOAUTH Authentication required.",
385            "WRONGPASS invalid username-password pair or user is disabled.",
386            "[collection_not_found] no such collection: docs",
387        ] {
388            let resp = Response::err(9, msg);
389            let frame = encode_frame(&resp).unwrap();
390            let (decoded, _): (Response, usize) = decode_frame(&frame).unwrap().unwrap();
391            assert_eq!(decoded.result, Err(msg.to_owned()));
392        }
393    }
394
395    // ── Framing edges (WIRE-020..023) ─────────────────────────────────────
396
397    #[test]
398    fn partial_header_and_partial_body_return_none() {
399        let frame = encode_frame(&Request {
400            id: 1,
401            command: "PING".to_owned(),
402            args: vec![],
403        })
404        .unwrap();
405        for cut in [0, 1, 3, 4, frame.len() - 1] {
406            let out: Option<(Request, usize)> = decode_frame(&frame[..cut]).unwrap();
407            assert!(out.is_none(), "cut at {cut} must ask for more bytes");
408        }
409    }
410
411    #[test]
412    fn two_frames_in_one_buffer_consume_exactly_one_each() {
413        let a = encode_frame(&Response::ok(1, Value::Int(1))).unwrap();
414        let b = encode_frame(&Response::ok(2, Value::Int(2))).unwrap();
415        let mut buf = a.clone();
416        buf.extend_from_slice(&b);
417        let (first, used): (Response, usize) = decode_frame(&buf).unwrap().unwrap();
418        assert_eq!(first.id, 1);
419        assert_eq!(used, a.len());
420        let (second, used2): (Response, usize) = decode_frame(&buf[used..]).unwrap().unwrap();
421        assert_eq!(second.id, 2);
422        assert_eq!(used2, b.len());
423    }
424
425    #[test]
426    fn oversized_prefix_rejected_before_body_arrives() {
427        // Only the 4-byte prefix claiming cap+1: the check fires without the
428        // body being present at all — allocation cannot have happened.
429        let over = (DEFAULT_MAX_FRAME_BYTES + 1) as u32;
430        let buf = over.to_le_bytes();
431        let err = decode_frame::<Request>(&buf).unwrap_err();
432        match err {
433            DecodeError::FrameTooLarge { body, max } => {
434                assert_eq!(body, DEFAULT_MAX_FRAME_BYTES + 1);
435                assert_eq!(max, DEFAULT_MAX_FRAME_BYTES);
436            }
437            other => panic!("expected FrameTooLarge, got {other:?}"),
438        }
439    }
440
441    #[test]
442    fn custom_limit_is_honored() {
443        let frame = encode_frame(&Value::Str("x".repeat(100))).unwrap();
444        let err = decode_frame_with_limit::<Value>(&frame, 8).unwrap_err();
445        assert!(matches!(err, DecodeError::FrameTooLarge { .. }));
446    }
447
448    #[test]
449    fn garbage_body_is_a_typed_error_not_a_panic() {
450        let mut buf = 4u32.to_le_bytes().to_vec();
451        buf.extend_from_slice(&[0xc1, 0xc1, 0xc1, 0xc1]); // 0xc1 is never valid
452        let err = decode_frame::<Request>(&buf).unwrap_err();
453        assert!(matches!(err, DecodeError::Rmp(_)));
454    }
455
456    #[test]
457    fn zero_length_body_is_a_keep_alive_not_a_parse_failure() {
458        // WIRE-024: still an error on the typed path — a zero-length body
459        // cannot be a Request — but a *named* one, so a caller can skip a
460        // liveness tick instead of pattern-matching a MessagePack error.
461        let buf = 0u32.to_le_bytes();
462        let err = decode_frame::<Request>(&buf).unwrap_err();
463        assert!(
464            matches!(err, DecodeError::KeepAlive),
465            "expected KeepAlive, got {err:?}"
466        );
467    }
468
469    // ── GH #6: borrowed-body decode (WIRE-022/024) ─────────────────────────
470
471    #[test]
472    fn raw_decode_borrows_the_body_and_reports_bytes_consumed() {
473        let frame = encode_frame(&Request {
474            id: 7,
475            command: "PING".to_owned(),
476            args: vec![],
477        })
478        .unwrap();
479
480        let (body, consumed) = decode_frame_raw(&frame, DEFAULT_MAX_FRAME_BYTES)
481            .unwrap()
482            .expect("a complete frame decodes");
483        assert_eq!(consumed, frame.len(), "consumed is 4 + body");
484        assert_eq!(body, &frame[4..], "the body is borrowed from the input");
485        // Borrowed, not copied: the slice points into the caller's buffer.
486        assert!(std::ptr::eq(body.as_ptr(), frame[4..].as_ptr()));
487
488        // And the typed path built on it agrees, byte for byte.
489        let (request, typed_consumed): (Request, usize) = decode_frame(&frame).unwrap().unwrap();
490        assert_eq!(typed_consumed, consumed);
491        assert_eq!(request.command, "PING");
492    }
493
494    #[test]
495    fn raw_decode_needs_more_bytes_for_a_partial_frame() {
496        let frame = encode_frame(&Request {
497            id: 1,
498            command: "ECHO".to_owned(),
499            args: vec![Value::bytes(vec![9u8; 64])],
500        })
501        .unwrap();
502
503        // Only a partial prefix.
504        assert!(decode_frame_raw(&frame[..3], DEFAULT_MAX_FRAME_BYTES)
505            .unwrap()
506            .is_none());
507        // Full prefix, partial body.
508        assert!(
509            decode_frame_raw(&frame[..frame.len() - 1], DEFAULT_MAX_FRAME_BYTES)
510                .unwrap()
511                .is_none()
512        );
513        // Complete.
514        assert!(decode_frame_raw(&frame, DEFAULT_MAX_FRAME_BYTES)
515            .unwrap()
516            .is_some());
517    }
518
519    #[test]
520    fn raw_decode_enforces_the_cap_before_slicing() {
521        // A prefix claiming 1 MiB with no body behind it: the cap must be
522        // judged from the prefix alone (WIRE-021), never by slicing first.
523        let mut buf = (1024u32 * 1024).to_le_bytes().to_vec();
524        buf.extend_from_slice(b"only a few bytes");
525        let err = decode_frame_raw(&buf, 4096).unwrap_err();
526        assert!(matches!(
527            err,
528            DecodeError::FrameTooLarge {
529                body: 1_048_576,
530                max: 4096
531            }
532        ));
533    }
534
535    #[test]
536    fn raw_decode_returns_a_keep_alive_as_an_empty_body() {
537        // WIRE-024: this is the shape that lets a product skip liveness ticks
538        // without wrapping the reader.
539        let buf = 0u32.to_le_bytes();
540        let (body, consumed) = decode_frame_raw(&buf, DEFAULT_MAX_FRAME_BYTES)
541            .unwrap()
542            .expect("a zero-length frame is a complete frame");
543        assert!(body.is_empty());
544        assert_eq!(consumed, 4, "prefix only");
545    }
546
547    #[test]
548    fn raw_decode_walks_a_buffer_of_several_frames() {
549        // The reuse case the issue describes: a product framing its own
550        // bodies drives the buffer with `consumed`, and never reimplements
551        // the prefix/cap/slice rules.
552        let mut stream = Vec::new();
553        for id in 1..=3u32 {
554            stream.extend_from_slice(
555                &encode_frame(&Request {
556                    id,
557                    command: "X".to_owned(),
558                    args: vec![],
559                })
560                .unwrap(),
561            );
562        }
563        // A keep-alive in the middle of the stream must not derail it.
564        stream.extend_from_slice(&0u32.to_le_bytes());
565
566        let mut at = 0;
567        let mut bodies = 0;
568        let mut keep_alives = 0;
569        while at < stream.len() {
570            let (body, consumed) = decode_frame_raw(&stream[at..], DEFAULT_MAX_FRAME_BYTES)
571                .unwrap()
572                .expect("each frame is complete");
573            if body.is_empty() {
574                keep_alives += 1;
575            } else {
576                bodies += 1;
577            }
578            at += consumed;
579        }
580        assert_eq!(bodies, 3);
581        assert_eq!(keep_alives, 1);
582        assert_eq!(at, stream.len(), "the buffer is fully consumed");
583    }
584
585    #[test]
586    fn push_id_is_reserved_u32_max() {
587        assert_eq!(PUSH_ID, u32::MAX);
588    }
589
590    // ── Async path (feature = "tokio") ────────────────────────────────────
591
592    #[cfg(feature = "tokio")]
593    #[tokio::test]
594    async fn async_write_then_read_reports_frame_size() {
595        let req = Request {
596            id: 3,
597            command: "PING".to_owned(),
598            args: vec![],
599        };
600        let mut buf = Vec::new();
601        let written = write_request(&mut buf, &req).await.unwrap();
602        assert_eq!(written, buf.len());
603        let mut cursor = std::io::Cursor::new(buf);
604        let (decoded, size) = read_request(&mut cursor).await.unwrap();
605        assert_eq!(decoded, req);
606        assert_eq!(size, written);
607    }
608
609    #[cfg(feature = "tokio")]
610    #[tokio::test]
611    async fn async_read_rejects_oversized_prefix_without_reading_body() {
612        let over = ((DEFAULT_MAX_FRAME_BYTES + 1) as u32).to_le_bytes();
613        let mut cursor = std::io::Cursor::new(over.to_vec());
614        let err = read_request(&mut cursor).await.unwrap_err();
615        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
616    }
617}