Skip to main content

aws_smithy_http_server/
body.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! HTTP body utilities.
7//!
8//! This module provides body handling utilities for HTTP 1.x using the
9//! `http-body` and `http-body-util` crates.
10
11use crate::error::{BoxError, Error};
12use bytes::Bytes;
13use std::fmt;
14
15// Used in the codegen in trait bounds.
16#[doc(hidden)]
17pub use http_body::Body as HttpBody;
18
19// ============================================================================
20// BoxBody - Type-Erased Body
21// ============================================================================
22
23/// The primary body type returned by the generated `smithy-rs` service.
24///
25/// This is a type-erased body that wraps `UnsyncBoxBody` from `http-body-util`.
26/// It is `Send` but not `Sync`, making it suitable for most HTTP handlers.
27pub type BoxBody = http_body_util::combinators::UnsyncBoxBody<Bytes, Error>;
28
29/// A thread-safe body type for operations that require `Sync`.
30///
31/// This is used specifically for event streaming operations and lambda handlers
32/// that need thread safety guarantees.
33pub type BoxBodySync = http_body_util::combinators::BoxBody<Bytes, Error>;
34
35// ============================================================================
36// Body Construction Functions
37// ============================================================================
38
39// `boxed` is used in the codegen of the implementation of the operation `Handler` trait.
40/// Convert an HTTP body implementing [`http_body::Body`] into a [`BoxBody`].
41pub fn boxed<B>(body: B) -> BoxBody
42where
43    B: http_body::Body<Data = Bytes> + Send + 'static,
44    B::Error: Into<BoxError>,
45{
46    use http_body_util::BodyExt;
47
48    try_downcast(body).unwrap_or_else(|body| body.map_err(Error::new).boxed_unsync())
49}
50
51/// Convert an HTTP body implementing [`http_body::Body`] into a [`BoxBodySync`].
52pub fn boxed_sync<B>(body: B) -> BoxBodySync
53where
54    B: http_body::Body<Data = Bytes> + Send + Sync + 'static,
55    B::Error: Into<BoxError>,
56{
57    use http_body_util::BodyExt;
58    body.map_err(Error::new).boxed()
59}
60
61#[doc(hidden)]
62pub(crate) fn try_downcast<T, K>(k: K) -> Result<T, K>
63where
64    T: 'static,
65    K: Send + 'static,
66{
67    let mut k = Some(k);
68    if let Some(k) = <dyn std::any::Any>::downcast_mut::<Option<T>>(&mut k) {
69        Ok(k.take().unwrap())
70    } else {
71        Err(k.unwrap())
72    }
73}
74
75/// Create an empty body.
76pub fn empty() -> BoxBody {
77    boxed(http_body_util::Empty::<Bytes>::new())
78}
79
80/// Create an empty sync body.
81pub fn empty_sync() -> BoxBodySync {
82    boxed_sync(http_body_util::Empty::<Bytes>::new())
83}
84
85/// Convert bytes or similar types into a [`BoxBody`].
86///
87/// This simplifies codegen a little bit.
88#[doc(hidden)]
89pub fn to_boxed<B>(body: B) -> BoxBody
90where
91    B: Into<Bytes>,
92{
93    boxed(http_body_util::Full::new(body.into()))
94}
95
96/// Convert bytes or similar types into a [`BoxBodySync`].
97///
98/// This simplifies codegen a little bit.
99#[doc(hidden)]
100pub fn to_boxed_sync<B>(body: B) -> BoxBodySync
101where
102    B: Into<Bytes>,
103{
104    boxed_sync(http_body_util::Full::new(body.into()))
105}
106
107/// Create a body from bytes.
108pub fn from_bytes(bytes: Bytes) -> BoxBody {
109    boxed(http_body_util::Full::new(bytes))
110}
111
112// ============================================================================
113// Size-limited Body Collection
114// ============================================================================
115
116/// An error produced by [`collect_body_limited`] when the body exceeds the configured limit.
117///
118/// This is intentionally a simple struct so it can be downcast via
119/// `Box<dyn std::error::Error>` from code that processes the error chain.
120#[doc(hidden)]
121#[derive(Debug, Clone, Copy)]
122pub struct BodyLimitExceeded {
123    /// The configured maximum, in bytes.
124    pub limit: usize,
125}
126
127impl fmt::Display for BodyLimitExceeded {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(
130            f,
131            "request body exceeded the configured maximum of {} bytes",
132            self.limit
133        )
134    }
135}
136
137impl std::error::Error for BodyLimitExceeded {}
138
139/// The error returned by [`collect_body_limited`].
140///
141/// Either the underlying body produced an error, or the configured size limit was
142/// exceeded. The generic over `E` lets the helper work with body error types that
143/// are `Send` but not `Sync` (the generated-server code carries only a `Send` bound),
144/// while still letting callers `?` the result into a `RequestRejection`.
145#[doc(hidden)]
146#[derive(Debug)]
147pub enum CollectBodyError<E> {
148    /// The underlying body produced an error while being read.
149    Body(E),
150    /// The body exceeded the configured maximum size.
151    TooLarge(BodyLimitExceeded),
152}
153
154impl<E: fmt::Display> fmt::Display for CollectBodyError<E> {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            Self::Body(e) => write!(f, "error reading request body: {e}"),
158            Self::TooLarge(e) => e.fmt(f),
159        }
160    }
161}
162
163impl<E: std::error::Error + 'static> std::error::Error for CollectBodyError<E> {
164    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
165        match self {
166            Self::Body(e) => Some(e),
167            Self::TooLarge(e) => Some(e),
168        }
169    }
170}
171
172/// Collect an HTTP body into a [`Bytes`] buffer, enforcing a maximum size.
173///
174/// If the body produces more than `limit` bytes (including via
175/// `Transfer-Encoding: chunked` or a mismatched `Content-Length`), the function
176/// returns an error *before* buffering additional data.
177///
178/// Passing `limit == 0` disables the check and collects the entire body (the
179/// historical behavior, *not* recommended — see the security notes on the
180/// `requestBodyMaxBytes` codegen setting).
181#[doc(hidden)]
182pub async fn collect_body_limited<B>(body: B, limit: usize) -> Result<Bytes, CollectBodyError<B::Error>>
183where
184    B: HttpBody,
185{
186    use http_body_util::BodyExt;
187
188    if limit == 0 {
189        return body
190            .collect()
191            .await
192            .map(|c| c.to_bytes())
193            .map_err(CollectBodyError::Body);
194    }
195
196    // Walk frames ourselves (rather than using `http_body_util::Limited`) so we
197    // don't require `B::Error: Send + Sync + 'static`. The generated server
198    // deserializer only carries a `Send` bound on body errors.
199    let lower = body.size_hint().lower() as usize;
200    if lower > limit {
201        return Err(CollectBodyError::TooLarge(BodyLimitExceeded { limit }));
202    }
203
204    let mut body = std::pin::pin!(body);
205    let mut collected = bytes::BytesMut::with_capacity(lower);
206    while let Some(frame) = std::future::poll_fn(|cx| body.as_mut().poll_frame(cx))
207        .await
208        .transpose()
209        .map_err(CollectBodyError::Body)?
210    {
211        if let Ok(data) = frame.into_data() {
212            use bytes::{Buf, BufMut};
213            let data_len = data.remaining();
214            if collected.len().saturating_add(data_len) > limit {
215                return Err(CollectBodyError::TooLarge(BodyLimitExceeded { limit }));
216            }
217            collected.put(data);
218        }
219        // Trailer / non-data frames are discarded; we don't surface trailers.
220    }
221
222    Ok(collected.freeze())
223}
224
225// ============================================================================
226// Stream Wrapping for Event Streaming
227// ============================================================================
228
229/// Wrap a stream of byte chunks into a BoxBody.
230///
231/// This is used for event streaming support. The stream should produce `Result<O, E>`
232/// where `O` can be converted into `Bytes` and `E` can be converted into an error.
233///
234/// In hyper 0.x, `Body::wrap_stream` was available directly on the body type.
235/// In hyper 1.x, the `stream` feature was removed, and the official approach is to use
236/// `http_body_util::StreamBody` to convert streams into bodies, which is what this
237/// function provides as a convenient wrapper.
238///
239/// For scenarios requiring `Sync` (e.g., lambda handlers), use [`wrap_stream_sync`] instead.
240pub fn wrap_stream<S, O, E>(stream: S) -> BoxBody
241where
242    S: futures_util::Stream<Item = Result<O, E>> + Send + 'static,
243    O: Into<Bytes> + 'static,
244    E: Into<BoxError> + 'static,
245{
246    use futures_util::TryStreamExt;
247    use http_body_util::StreamBody;
248
249    // Convert the stream of Result<O, E> into a stream of Result<Frame<Bytes>, Error>
250    let frame_stream = stream
251        .map_ok(|chunk| http_body::Frame::data(chunk.into()))
252        .map_err(|e| Error::new(e.into()));
253
254    boxed(StreamBody::new(frame_stream))
255}
256
257/// Wrap a stream of byte chunks into a BoxBodySync.
258///
259/// This is the thread-safe variant of [`wrap_stream`], used for event streaming operations
260/// that require `Sync` bounds, such as lambda handlers.
261///
262/// The stream should produce `Result<O, E>` where `O` can be converted into `Bytes` and
263/// `E` can be converted into an error.
264pub fn wrap_stream_sync<S, O, E>(stream: S) -> BoxBodySync
265where
266    S: futures_util::Stream<Item = Result<O, E>> + Send + Sync + 'static,
267    O: Into<Bytes> + 'static,
268    E: Into<BoxError> + 'static,
269{
270    use futures_util::TryStreamExt;
271    use http_body_util::StreamBody;
272
273    // Convert the stream of Result<O, E> into a stream of Result<Frame<Bytes>, Error>
274    let frame_stream = stream
275        .map_ok(|chunk| http_body::Frame::data(chunk.into()))
276        .map_err(|e| Error::new(e.into()));
277
278    boxed_sync(StreamBody::new(frame_stream))
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    /// Collect all bytes from a body (test utility).
286    ///
287    /// This uses `http_body_util::BodyExt::collect()` to read all body chunks
288    /// into a single `Bytes` buffer.
289    async fn collect_bytes<B>(body: B) -> Result<Bytes, Error>
290    where
291        B: HttpBody,
292        B::Error: Into<BoxError>,
293    {
294        use http_body_util::BodyExt;
295
296        let collected = body.collect().await.map_err(Error::new)?;
297        Ok(collected.to_bytes())
298    }
299
300    #[tokio::test]
301    async fn test_empty_body() {
302        let body = empty();
303        let bytes = collect_bytes(body).await.unwrap();
304        assert_eq!(bytes.len(), 0);
305    }
306
307    #[tokio::test]
308    async fn test_from_bytes() {
309        let data = Bytes::from("hello world");
310        let body = from_bytes(data.clone());
311        let collected = collect_bytes(body).await.unwrap();
312        assert_eq!(collected, data);
313    }
314
315    #[tokio::test]
316    async fn test_to_boxed_string() {
317        let s = "hello world";
318        let body = to_boxed(s);
319        let collected = collect_bytes(body).await.unwrap();
320        assert_eq!(collected, Bytes::from(s));
321    }
322
323    #[tokio::test]
324    async fn test_to_boxed_vec() {
325        let vec = vec![1u8, 2, 3, 4, 5];
326        let body = to_boxed(vec.clone());
327        let collected = collect_bytes(body).await.unwrap();
328        assert_eq!(collected.as_ref(), vec.as_slice());
329    }
330
331    #[tokio::test]
332    async fn test_boxed() {
333        use http_body_util::Full;
334        let full_body = Full::new(Bytes::from("test data"));
335        let boxed_body: BoxBody = boxed(full_body);
336        let collected = collect_bytes(boxed_body).await.unwrap();
337        assert_eq!(collected, Bytes::from("test data"));
338    }
339
340    #[tokio::test]
341    async fn test_boxed_sync() {
342        use http_body_util::Full;
343        let full_body = Full::new(Bytes::from("sync test"));
344        let boxed_body: BoxBodySync = boxed_sync(full_body);
345        let collected = collect_bytes(boxed_body).await.unwrap();
346        assert_eq!(collected, Bytes::from("sync test"));
347    }
348
349    #[tokio::test]
350    async fn test_wrap_stream_single_chunk() {
351        use futures_util::stream;
352
353        let data = Bytes::from("single chunk");
354        let stream = stream::iter(vec![Ok::<_, std::io::Error>(data.clone())]);
355
356        let body = wrap_stream(stream);
357        let collected = collect_bytes(body).await.unwrap();
358        assert_eq!(collected, data);
359    }
360
361    #[tokio::test]
362    async fn test_wrap_stream_multiple_chunks() {
363        use futures_util::stream;
364
365        let chunks = vec![
366            Ok::<_, std::io::Error>(Bytes::from("chunk1")),
367            Ok(Bytes::from("chunk2")),
368            Ok(Bytes::from("chunk3")),
369        ];
370        let expected = Bytes::from("chunk1chunk2chunk3");
371
372        let stream = stream::iter(chunks);
373        let body = wrap_stream(stream);
374        let collected = collect_bytes(body).await.unwrap();
375        assert_eq!(collected, expected);
376    }
377
378    #[tokio::test]
379    async fn test_wrap_stream_empty() {
380        use futures_util::stream;
381
382        let stream = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::new())]);
383
384        let body = wrap_stream(stream);
385        let collected = collect_bytes(body).await.unwrap();
386        assert_eq!(collected.len(), 0);
387    }
388
389    #[tokio::test]
390    async fn test_wrap_stream_error() {
391        use futures_util::stream;
392
393        let chunks = vec![
394            Ok::<_, std::io::Error>(Bytes::from("chunk1")),
395            Err(std::io::Error::other("test error")),
396        ];
397
398        let stream = stream::iter(chunks);
399        let body = wrap_stream(stream);
400        let result = collect_bytes(body).await;
401        assert!(result.is_err());
402    }
403
404    #[tokio::test]
405    async fn test_wrap_stream_various_types() {
406        use futures_util::stream;
407
408        // Test that Into<Bytes> works for various types
409
410        // Test with &str
411        let chunks = vec![Ok::<_, std::io::Error>("string slice"), Ok("another string")];
412        let stream = stream::iter(chunks);
413        let body = wrap_stream(stream);
414        let collected = collect_bytes(body).await.unwrap();
415        assert_eq!(collected, Bytes::from("string sliceanother string"));
416
417        // Test with String
418        let chunks = vec![
419            Ok::<_, std::io::Error>(String::from("owned ")),
420            Ok(String::from("strings")),
421        ];
422        let stream = stream::iter(chunks);
423        let body = wrap_stream(stream);
424        let collected = collect_bytes(body).await.unwrap();
425        assert_eq!(collected, Bytes::from("owned strings"));
426
427        // Test with Vec<u8>
428        let chunks = vec![
429            Ok::<_, std::io::Error>(vec![72u8, 101, 108, 108, 111]), // "Hello"
430            Ok(vec![32u8, 87, 111, 114, 108, 100]),                  // " World"
431        ];
432        let stream = stream::iter(chunks);
433        let body = wrap_stream(stream);
434        let collected = collect_bytes(body).await.unwrap();
435        assert_eq!(collected, Bytes::from("Hello World"));
436
437        // Test with &[u8]
438        let chunks = vec![
439            Ok::<_, std::io::Error>(&[98u8, 121, 116, 101] as &[u8]), // "byte"
440            Ok(&[115u8, 33] as &[u8]),                                // "s!"
441        ];
442        let stream = stream::iter(chunks);
443        let body = wrap_stream(stream);
444        let collected = collect_bytes(body).await.unwrap();
445        assert_eq!(collected, Bytes::from("bytes!"));
446
447        // Test with custom struct implementing Into<Bytes>
448        struct CustomChunk {
449            data: String,
450        }
451
452        impl From<CustomChunk> for Bytes {
453            fn from(chunk: CustomChunk) -> Bytes {
454                Bytes::from(chunk.data)
455            }
456        }
457
458        let chunks = vec![
459            Ok::<_, std::io::Error>(CustomChunk { data: "custom ".into() }),
460            Ok(CustomChunk { data: "struct".into() }),
461        ];
462        let stream = stream::iter(chunks);
463        let body = wrap_stream(stream);
464        let collected = collect_bytes(body).await.unwrap();
465        assert_eq!(collected, Bytes::from("custom struct"));
466    }
467
468    #[tokio::test]
469    async fn test_wrap_stream_custom_stream_type() {
470        use bytes::Bytes;
471        use std::pin::Pin;
472        use std::task::{Context, Poll};
473
474        // Custom stream type that implements futures_util::Stream
475        struct CustomStream {
476            chunks: Vec<Result<Bytes, std::io::Error>>,
477        }
478
479        impl CustomStream {
480            fn new(chunks: Vec<Result<Bytes, std::io::Error>>) -> Self {
481                Self { chunks }
482            }
483        }
484
485        impl futures_util::Stream for CustomStream {
486            type Item = Result<Bytes, std::io::Error>;
487
488            fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
489                if self.chunks.is_empty() {
490                    Poll::Ready(None)
491                } else {
492                    Poll::Ready(Some(self.chunks.remove(0)))
493                }
494            }
495        }
496
497        let stream = CustomStream::new(vec![Ok(Bytes::from("custom ")), Ok(Bytes::from("stream"))]);
498
499        let body = wrap_stream(stream);
500        let collected = collect_bytes(body).await.unwrap();
501        assert_eq!(collected, Bytes::from("custom stream"));
502    }
503
504    #[tokio::test]
505    async fn test_wrap_stream_custom_error_type() {
506        use bytes::Bytes;
507        use futures_util::stream;
508
509        // Custom error type that implements Into<BoxError>
510        #[derive(Debug, Clone)]
511        struct CustomError {
512            message: String,
513        }
514
515        impl std::fmt::Display for CustomError {
516            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517                write!(f, "CustomError: {}", self.message)
518            }
519        }
520
521        impl std::error::Error for CustomError {}
522
523        // Test successful case with custom error type
524        let chunks = vec![
525            Ok::<_, CustomError>(Bytes::from("custom ")),
526            Ok(Bytes::from("error type")),
527        ];
528        let stream = stream::iter(chunks);
529        let body = wrap_stream(stream);
530        let collected = collect_bytes(body).await.unwrap();
531        assert_eq!(collected, Bytes::from("custom error type"));
532
533        // Test error case with custom error type
534        let chunks = vec![
535            Ok::<_, CustomError>(Bytes::from("data")),
536            Err(CustomError {
537                message: "custom error".into(),
538            }),
539        ];
540        let stream = stream::iter(chunks);
541        let body = wrap_stream(stream);
542        let result = collect_bytes(body).await;
543        assert!(result.is_err());
544    }
545
546    #[tokio::test]
547    async fn test_wrap_stream_incremental_consumption() {
548        use bytes::Bytes;
549        use http_body_util::BodyExt;
550        use std::pin::Pin;
551        use std::task::{Context, Poll};
552
553        struct IncrementalStream {
554            chunks: Vec<Result<Bytes, std::io::Error>>,
555        }
556
557        impl IncrementalStream {
558            fn new(chunks: Vec<Result<Bytes, std::io::Error>>) -> Self {
559                Self { chunks }
560            }
561        }
562
563        impl futures_util::Stream for IncrementalStream {
564            type Item = Result<Bytes, std::io::Error>;
565
566            fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
567                if self.chunks.is_empty() {
568                    Poll::Ready(None)
569                } else {
570                    Poll::Ready(Some(self.chunks.remove(0)))
571                }
572            }
573        }
574
575        let stream = IncrementalStream::new(vec![
576            Ok(Bytes::from("chunk1")),
577            Ok(Bytes::from("chunk2")),
578            Ok(Bytes::from("chunk3")),
579        ]);
580
581        let mut body = wrap_stream(stream);
582
583        let frame1 = body.frame().await.unwrap().unwrap();
584        assert!(frame1.is_data());
585        assert_eq!(frame1.into_data().unwrap(), Bytes::from("chunk1"));
586
587        let frame2 = body.frame().await.unwrap().unwrap();
588        assert!(frame2.is_data());
589        assert_eq!(frame2.into_data().unwrap(), Bytes::from("chunk2"));
590
591        let frame3 = body.frame().await.unwrap().unwrap();
592        assert!(frame3.is_data());
593        assert_eq!(frame3.into_data().unwrap(), Bytes::from("chunk3"));
594
595        let frame4 = body.frame().await;
596        assert!(frame4.is_none());
597    }
598
599    #[tokio::test]
600    async fn test_wrap_stream_sync_single_chunk() {
601        use futures_util::stream;
602
603        let data = Bytes::from("sync single chunk");
604        let stream = stream::iter(vec![Ok::<_, std::io::Error>(data.clone())]);
605
606        let body = wrap_stream_sync(stream);
607        let collected = collect_bytes(body).await.unwrap();
608        assert_eq!(collected, data);
609    }
610
611    #[tokio::test]
612    async fn test_wrap_stream_sync_multiple_chunks() {
613        use futures_util::stream;
614
615        let chunks = vec![
616            Ok::<_, std::io::Error>(Bytes::from("sync1")),
617            Ok(Bytes::from("sync2")),
618            Ok(Bytes::from("sync3")),
619        ];
620        let expected = Bytes::from("sync1sync2sync3");
621
622        let stream = stream::iter(chunks);
623        let body = wrap_stream_sync(stream);
624        let collected = collect_bytes(body).await.unwrap();
625        assert_eq!(collected, expected);
626    }
627
628    #[tokio::test]
629    async fn test_empty_sync_body() {
630        let body = empty_sync();
631        let bytes = collect_bytes(body).await.unwrap();
632        assert_eq!(bytes.len(), 0);
633    }
634
635    #[tokio::test]
636    async fn test_to_boxed_sync() {
637        let data = Bytes::from("sync boxed data");
638        let body = to_boxed_sync(data.clone());
639        let collected = collect_bytes(body).await.unwrap();
640        assert_eq!(collected, data);
641    }
642
643    // Compile-time tests to ensure Send/Sync bounds are correct
644    // Following the pattern used by hyper and axum
645    fn _assert_send<T: Send>() {}
646    fn _assert_sync<T: Sync>() {}
647
648    fn _assert_send_sync_bounds() {
649        // BoxBodySync must be both Send and Sync
650        _assert_send::<BoxBodySync>();
651        _assert_sync::<BoxBodySync>();
652
653        // BoxBody must be Send (but is intentionally NOT Sync - it's UnsyncBoxBody)
654        _assert_send::<BoxBody>();
655    }
656
657    #[tokio::test]
658    async fn test_wrap_stream_sync_produces_sync_body() {
659        use futures_util::stream;
660
661        let data = Bytes::from("test sync");
662        let stream = stream::iter(vec![Ok::<_, std::io::Error>(data.clone())]);
663
664        let body = wrap_stream_sync(stream);
665
666        // Compile-time check: ensure the body is Sync
667        fn check_sync<T: Sync>(_: &T) {}
668        check_sync(&body);
669
670        let collected = collect_bytes(body).await.unwrap();
671        assert_eq!(collected, data);
672    }
673
674    #[test]
675    fn test_empty_sync_is_sync() {
676        let body = empty_sync();
677        fn check_sync<T: Sync>(_: &T) {}
678        check_sync(&body);
679    }
680
681    #[test]
682    fn test_boxed_sync_is_sync() {
683        use http_body_util::Full;
684        let body = boxed_sync(Full::new(Bytes::from("test")));
685        fn check_sync<T: Sync>(_: &T) {}
686        check_sync(&body);
687    }
688
689    #[tokio::test]
690    async fn test_collect_body_limited_under_limit() {
691        let body = from_bytes(Bytes::from("hello"));
692        let result = collect_body_limited(body, 1024).await;
693        assert_eq!(result.unwrap(), Bytes::from("hello"));
694    }
695
696    #[tokio::test]
697    async fn test_collect_body_limited_over_limit() {
698        let body = from_bytes(Bytes::from("this is way too long"));
699        let result = collect_body_limited(body, 5).await;
700        assert!(matches!(
701            result,
702            Err(CollectBodyError::TooLarge(BodyLimitExceeded { limit: 5 }))
703        ));
704    }
705
706    #[tokio::test]
707    async fn test_collect_body_limited_chunked_exceeds_mid_stream() {
708        use futures_util::stream;
709
710        // Simulate a chunked body where the limit is crossed on the second chunk.
711        let chunks = vec![
712            Ok::<_, std::io::Error>(http_body::Frame::data(Bytes::from("aaaa"))), // 4 bytes
713            Ok(http_body::Frame::data(Bytes::from("bbbb"))),                      // +4 = 8, over limit of 6
714        ];
715        let body = http_body_util::StreamBody::new(stream::iter(chunks));
716        let result = collect_body_limited(body, 6).await;
717        assert!(matches!(result, Err(CollectBodyError::TooLarge(_))));
718    }
719
720    #[tokio::test]
721    async fn test_collect_body_limited_empty_body() {
722        let body = empty();
723        let result = collect_body_limited(body, 100).await;
724        assert_eq!(result.unwrap(), Bytes::from(""));
725    }
726}