Skip to main content

trillium_http/
received_body.rs

1use crate::{Body, Buffer, Error, Headers, HttpConfig, MutCow, ProtocolSession, copy};
2use Poll::{Pending, Ready};
3use ReceivedBodyState::{Chunked, End, PartialChunkSize, Raw};
4use encoding_rs::Encoding;
5use futures_lite::{AsyncRead, AsyncWrite, ready};
6use std::{
7    borrow::Cow,
8    fmt::{self, Debug, Formatter},
9    io::{self, ErrorKind},
10    pin::Pin,
11    task::{Context, Poll},
12};
13
14mod chunked;
15mod h3_data;
16mod raw;
17
18pub(crate) use chunked::write_chunk;
19
20/// A received http body
21///
22/// This type represents a body that will be read from the underlying transport, which it may either
23/// borrow from a [`Conn`](crate::Conn) or own.
24///
25/// ```rust
26/// # use trillium_testing::HttpTest;
27/// let app = HttpTest::new(|mut conn| async move {
28///     let body = conn.request_body();
29///     let body_string = body.read_string().await.unwrap();
30///     conn.with_response_body(format!("received: {body_string}"))
31/// });
32///
33/// app.get("/").block().assert_body("received: ");
34/// app.post("/")
35///     .with_body("hello")
36///     .block()
37///     .assert_body("received: hello");
38/// ```
39///
40/// ## Bounds checking
41///
42/// Every `ReceivedBody` has a maximum length beyond which it will return an error, expressed as a
43/// u64. To override this on the specific `ReceivedBody`, use [`ReceivedBody::with_max_len`] or
44/// [`ReceivedBody::set_max_len`]
45///
46/// The default maximum length is 10mb; see [`HttpConfig::received_body_max_len`] to configure
47/// this server-wide.
48///
49/// ## Large chunks, small read buffers
50///
51/// Attempting to read a chunked body with a buffer that is shorter than the chunk size in hex will
52/// result in an error.
53#[derive(fieldwork::Fieldwork)]
54pub struct ReceivedBody<'conn, Transport> {
55    /// The content-length of this body, derived from the content-length header.
56    /// `None` for transfer-encoding chunked bodies.
57    ///
58    /// ```rust
59    /// # use trillium_testing::HttpTest;
60    /// HttpTest::new(|mut conn| async move {
61    ///     let body = conn.request_body();
62    ///     assert_eq!(body.content_length(), Some(5));
63    ///     let body_string = body.read_string().await.unwrap();
64    ///     conn.with_status(200)
65    ///         .with_response_body(format!("received: {body_string}"))
66    /// })
67    /// .post("/")
68    /// .with_body("hello")
69    /// .block()
70    /// .assert_ok()
71    /// .assert_body("received: hello");
72    /// ```
73    #[field(get)]
74    content_length: Option<u64>,
75
76    buffer: MutCow<'conn, Buffer>,
77
78    transport: Option<MutCow<'conn, Transport>>,
79
80    state: MutCow<'conn, ReceivedBodyState>,
81
82    on_completion: Option<Box<dyn FnOnce(Transport) + Send + Sync + 'static>>,
83
84    /// the character encoding of this body, usually determined from the content type
85    /// (mime-type) of the associated Conn.
86    #[field(get)]
87    encoding: &'static Encoding,
88
89    /// The maximum length that can be read from this body before error
90    ///
91    /// See also [`HttpConfig::received_body_max_len`]
92    #[field(with, get, set)]
93    max_len: u64,
94
95    /// The initial buffer capacity allocated when reading the body to bytes or a string
96    ///
97    /// See [`HttpConfig::received_body_initial_len`]
98    #[field(with, get, set)]
99    initial_len: usize,
100
101    /// The maximum number of read loops that reading this received body will perform before
102    /// yielding back to the runtime
103    ///
104    /// See [`HttpConfig::copy_loops_per_yield`]
105    #[field(with, get, set)]
106    copy_loops_per_yield: usize,
107
108    /// Maximum size to pre-allocate based on content-length for buffering this received body
109    ///
110    /// See [`HttpConfig::received_body_max_preallocate`]
111    #[field(with, get, set)]
112    max_preallocate: usize,
113
114    max_header_list_size: u64,
115
116    trailers: MutCow<'conn, Option<Headers>>,
117
118    /// Byte offset into `b"HTTP/1.1 100 Continue\r\n\r\n"` that remains to be written before the
119    /// first read. `None` means no pending write.
120    send_100_continue_offset: Option<usize>,
121
122    /// Protocol session this body belongs to; used by the `End` transition to pull
123    /// driver-decoded trailers (h2 synchronously, h3 asynchronously).
124    protocol_session: ProtocolSession,
125
126    /// Pending h3 trailer-decode future
127    h3_trailer_future: MutCow<'conn, Option<H3TrailerFuture>>,
128
129    /// Accumulator for the QPACK-encoded trailer payload
130    h3_trailer_payload_buffer: MutCow<'conn, Vec<u8>>,
131}
132
133/// Boxed future returned by the QPACK decoder for trailing HEADERS on an h3 body.
134pub(crate) type H3TrailerFuture =
135    Pin<Box<dyn Future<Output = io::Result<Headers>> + Send + Sync + 'static>>;
136
137fn slice_from(min: u64, buf: &[u8]) -> Option<&[u8]> {
138    buf.get(usize::try_from(min).unwrap_or(usize::MAX)..)
139        .filter(|buf| !buf.is_empty())
140}
141
142impl<'conn, Transport> ReceivedBody<'conn, Transport>
143where
144    Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
145{
146    #[allow(missing_docs)]
147    #[doc(hidden)]
148    pub fn new(
149        content_length: Option<u64>,
150        buffer: impl Into<MutCow<'conn, Buffer>>,
151        transport: impl Into<MutCow<'conn, Transport>>,
152        state: impl Into<MutCow<'conn, ReceivedBodyState>>,
153        on_completion: Option<Box<dyn FnOnce(Transport) + Send + Sync + 'static>>,
154        encoding: &'static Encoding,
155    ) -> Self {
156        Self::new_with_config(
157            content_length,
158            buffer,
159            transport,
160            state,
161            on_completion,
162            encoding,
163            &HttpConfig::DEFAULT,
164        )
165    }
166
167    #[allow(missing_docs)]
168    #[doc(hidden)]
169    pub(crate) fn new_with_config(
170        content_length: Option<u64>,
171        buffer: impl Into<MutCow<'conn, Buffer>>,
172        transport: impl Into<MutCow<'conn, Transport>>,
173        state: impl Into<MutCow<'conn, ReceivedBodyState>>,
174        on_completion: Option<Box<dyn FnOnce(Transport) + Send + Sync + 'static>>,
175        encoding: &'static Encoding,
176        config: &HttpConfig,
177    ) -> Self {
178        Self {
179            content_length,
180            buffer: buffer.into(),
181            transport: Some(transport.into()),
182            state: state.into(),
183            on_completion,
184            encoding,
185            max_len: config.received_body_max_len,
186            initial_len: config.received_body_initial_len,
187            copy_loops_per_yield: config.copy_loops_per_yield,
188            max_preallocate: config.received_body_max_preallocate,
189            max_header_list_size: config.max_header_list_size,
190            trailers: None.into(),
191            send_100_continue_offset: None,
192            protocol_session: ProtocolSession::Http1,
193            h3_trailer_future: None.into(),
194            h3_trailer_payload_buffer: Vec::new().into(),
195        }
196    }
197
198    /// Park the QPACK trailer-decode future in caller-owned storage. Required when this
199    /// body is rebuilt per `poll_read` (the future's registered waker would otherwise be
200    /// dropped along with the future on `Pending`).
201    #[must_use]
202    pub(crate) fn with_h3_trailer_future(
203        mut self,
204        future: impl Into<MutCow<'conn, Option<H3TrailerFuture>>>,
205    ) -> Self {
206        self.h3_trailer_future = future.into();
207        self
208    }
209
210    /// Park the QPACK trailer-payload accumulator in caller-owned storage. Required when
211    /// this body is rebuilt per `poll_read` so the partial accumulation persists across
212    /// polls.
213    #[must_use]
214    pub(crate) fn with_h3_trailer_payload_buffer(
215        mut self,
216        buffer: impl Into<MutCow<'conn, Vec<u8>>>,
217    ) -> Self {
218        self.h3_trailer_payload_buffer = buffer.into();
219        self
220    }
221
222    /// Sets the destination for trailers decoded from the request body.
223    ///
224    /// When the body is fully read, any trailers will be written to the provided storage.
225    #[doc(hidden)]
226    #[must_use]
227    pub fn with_trailers(mut self, trailers: impl Into<MutCow<'conn, Option<Headers>>>) -> Self {
228        self.trailers = trailers.into();
229        self
230    }
231
232    /// Associate this body with the [`ProtocolSession`] that produced it. The End
233    /// transition of the body state machine consults this to pull driver-decoded
234    /// trailers into [`Conn::request_trailers`][crate::Conn] (h2 synchronously,
235    /// h3 via a boxed future). For h1 bodies the session is
236    /// [`ProtocolSession::Http1`] and no trailer-driver hook fires.
237    #[doc(hidden)]
238    #[must_use]
239    #[cfg(feature = "unstable")]
240    pub fn with_protocol_session(mut self, protocol_session: ProtocolSession) -> Self {
241        self.protocol_session = protocol_session;
242        self
243    }
244
245    #[doc(hidden)]
246    #[must_use]
247    #[cfg(not(feature = "unstable"))]
248    pub(crate) fn with_protocol_session(mut self, protocol_session: ProtocolSession) -> Self {
249        self.protocol_session = protocol_session;
250        self
251    }
252
253    /// Arranges for `HTTP/1.1 100 Continue\r\n\r\n` to be written to the transport before the
254    /// first body read. Used to implement lazy 100-continue for HTTP/1.1 request bodies.
255    #[must_use]
256    pub(crate) fn with_send_100_continue(mut self) -> Self {
257        self.send_100_continue_offset = Some(0);
258        self
259    }
260
261    /// # Reads entire body to String.
262    ///
263    /// This uses the encoding determined by the content-type (mime) charset. If an
264    /// encoding problem is encountered, the returned String will contain utf8
265    /// replacement characters.
266    ///
267    /// Can only be performed once per Conn — the body bytes are not cached.
268    ///
269    /// # Errors
270    ///
271    /// This will return an error if there is an IO error on the
272    /// underlying transport such as a disconnect
273    ///
274    /// This will also return an error if the length exceeds the maximum length. To override this
275    /// value on this specific body, use [`ReceivedBody::with_max_len`] or
276    /// [`ReceivedBody::set_max_len`]
277    pub async fn read_string(self) -> crate::Result<String> {
278        let encoding = self.encoding();
279        let bytes = self.read_bytes().await?;
280        let (decoded, _, _) = encoding.decode(&bytes);
281        if let Cow::Borrowed(decoded) = decoded
282            && decoded.len() == bytes.len()
283        {
284            // The bytes decoded to themselves, so they can become the string without a copy.
285            return Ok(String::from_utf8(bytes)
286                .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()));
287        }
288        Ok(decoded.into_owned())
289    }
290
291    fn owns_transport(&self) -> bool {
292        self.transport.as_ref().is_some_and(MutCow::is_owned)
293    }
294
295    /// Similar to [`ReceivedBody::read_string`], but returns the raw bytes. This is useful for
296    /// bodies that are not text.
297    ///
298    /// You can use this in conjunction with `encoding` if you need different handling of malformed
299    /// character encoding than the lossy conversion provided by [`ReceivedBody::read_string`].
300    ///
301    /// # Errors
302    ///
303    /// This will return an error if there is an IO error on the underlying transport such as a
304    /// disconnect
305    ///
306    /// This will also return an error if the length exceeds
307    /// [`received_body_max_len`][HttpConfig::with_received_body_max_len]. To override this value on
308    /// this specific body, use [`ReceivedBody::with_max_len`] or [`ReceivedBody::set_max_len`]
309    pub async fn read_bytes(mut self) -> crate::Result<Vec<u8>> {
310        let mut vec = if let Some(len) = self.content_length {
311            if len > self.max_len {
312                return Err(Error::ReceivedBodyTooLong(self.max_len));
313            }
314
315            let len = usize::try_from(len).map_err(|_| Error::ReceivedBodyTooLong(self.max_len))?;
316
317            Vec::with_capacity(len.min(self.max_preallocate))
318        } else {
319            Vec::with_capacity(self.initial_len)
320        };
321
322        crate::util::read_to_end(&mut self, &mut vec).await?;
323        Ok(vec)
324    }
325
326    fn read_raw(&mut self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<io::Result<usize>> {
327        if let Some(transport) = self.transport.as_deref_mut() {
328            read_buffered(&mut self.buffer, transport, cx, buf)
329        } else {
330            Ready(Err(ErrorKind::NotConnected.into()))
331        }
332    }
333
334    /// Consumes the remainder of this body from the underlying transport by reading it to the end
335    /// and discarding the contents. This is important for http1.1 keepalive, but most of the
336    /// time you do not need to directly call this. It returns the number of bytes consumed.
337    ///
338    /// # Errors
339    ///
340    /// This will return an [`std::io::Result::Err`] if there is an io error on the underlying
341    /// transport, such as a disconnect
342    #[allow(
343        clippy::missing_errors_doc,
344        reason = "errors are documented above; clippy doesn't detect the section"
345    )]
346    pub async fn drain(self) -> io::Result<u64> {
347        let copy_loops_per_yield = self.copy_loops_per_yield;
348        copy(self, futures_lite::io::sink(), copy_loops_per_yield).await
349    }
350}
351
352impl<T> ReceivedBody<'static, T> {
353    /// takes the static transport from this received body
354    pub fn take_transport(&mut self) -> Option<T> {
355        self.transport.take().map(MutCow::unwrap_owned)
356    }
357
358    #[doc(hidden)]
359    #[cfg(feature = "unstable")]
360    pub fn state(&self) -> ReceivedBodyState {
361        *self.state
362    }
363}
364
365impl<T> ReceivedBody<'_, T> {
366    /// Borrow the trailers decoded from this body, if any. Unlike [`BodySource::trailers`],
367    /// this does not take them. Only `Some` after the body has been read to end-of-stream on
368    /// a protocol that carried a trailer section.
369    ///
370    /// [`BodySource::trailers`]: crate::BodySource::trailers
371    #[doc(hidden)]
372    #[cfg(feature = "unstable")]
373    pub fn trailers_ref(&self) -> Option<&Headers> {
374        self.trailers.as_ref()
375    }
376
377    /// Retype as `ReceivedBody<'static, T>` if every internal `MutCow` field is `Owned`.
378    ///
379    /// Returns `None` if any field is `Borrowed`, in which case `self` is dropped — the
380    /// borrows can't be extended, and there's no useful way to hand a half-destructured
381    /// body back. For callers whose runtime invariants guarantee ownership but whose
382    /// type-level `'a` parameter the compiler can't see is `'static`.
383    #[doc(hidden)]
384    #[cfg(feature = "unstable")]
385    pub fn try_into_owned(self) -> Option<ReceivedBody<'static, T>> {
386        let Self {
387            content_length,
388            buffer,
389            transport,
390            state,
391            on_completion,
392            encoding,
393            max_len,
394            initial_len,
395            copy_loops_per_yield,
396            max_preallocate,
397            max_header_list_size,
398            trailers,
399            send_100_continue_offset,
400            protocol_session,
401            h3_trailer_future,
402            h3_trailer_payload_buffer,
403        } = self;
404
405        let transport = match transport {
406            None => None,
407            Some(t) => Some(t.try_into_owned()?),
408        };
409
410        Some(ReceivedBody {
411            content_length,
412            buffer: buffer.try_into_owned()?,
413            transport,
414            state: state.try_into_owned()?,
415            on_completion,
416            encoding,
417            max_len,
418            initial_len,
419            copy_loops_per_yield,
420            max_preallocate,
421            max_header_list_size,
422            trailers: trailers.try_into_owned()?,
423            send_100_continue_offset,
424            protocol_session,
425            h3_trailer_future: h3_trailer_future.try_into_owned()?,
426            h3_trailer_payload_buffer: h3_trailer_payload_buffer.try_into_owned()?,
427        })
428    }
429}
430
431pub(crate) fn read_buffered<Transport>(
432    buffer: &mut Buffer,
433    transport: &mut Transport,
434    cx: &mut Context<'_>,
435    buf: &mut [u8],
436) -> Poll<io::Result<usize>>
437where
438    Transport: AsyncRead + Unpin,
439{
440    if buffer.is_empty() {
441        Pin::new(transport).poll_read(cx, buf)
442    } else if buffer.len() >= buf.len() {
443        let len = buf.len();
444        buf.copy_from_slice(&buffer[..len]);
445        buffer.ignore_front(len);
446        Ready(Ok(len))
447    } else {
448        let self_buffer_len = buffer.len();
449        buf[..self_buffer_len].copy_from_slice(buffer);
450        buffer.clear();
451        match Pin::new(transport).poll_read(cx, &mut buf[self_buffer_len..]) {
452            Ready(Ok(additional)) => Ready(Ok(additional + self_buffer_len)),
453            Pending => Ready(Ok(self_buffer_len)),
454            other @ Ready(_) => other,
455        }
456    }
457}
458
459type StateOutput = Poll<io::Result<(ReceivedBodyState, usize)>>;
460
461impl<Transport> AsyncRead for ReceivedBody<'_, Transport>
462where
463    Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
464{
465    fn poll_read(
466        mut self: Pin<&mut Self>,
467        cx: &mut Context<'_>,
468        buf: &mut [u8],
469    ) -> Poll<io::Result<usize>> {
470        const CONTINUE: &[u8] = b"HTTP/1.1 100 Continue\r\n\r\n";
471        while let Some(offset) = self.send_100_continue_offset {
472            let n = {
473                let Some(transport) = self.transport.as_deref_mut() else {
474                    return Ready(Err(ErrorKind::NotConnected.into()));
475                };
476                if offset == 0 {
477                    log::trace!("sending 100-continue");
478                }
479                ready!(Pin::new(transport).poll_write(cx, &CONTINUE[offset..]))?
480            };
481            if n == 0 {
482                return Ready(Err(ErrorKind::WriteZero.into()));
483            }
484            let new_offset = offset + n;
485            self.send_100_continue_offset = if new_offset >= CONTINUE.len() {
486                None
487            } else {
488                Some(new_offset)
489            };
490        }
491
492        for _ in 0..self.copy_loops_per_yield {
493            let (new_body_state, bytes) = ready!(match *self.state {
494                Chunked { remaining, total } => self.handle_chunked(cx, buf, remaining, total),
495                PartialChunkSize { total } => self.handle_partial(cx, buf, total),
496                Raw { total } => self.handle_raw(cx, buf, total),
497                ReceivedBodyState::H3Data {
498                    remaining_in_frame,
499                    total,
500                    frame_type,
501                    partial_frame_header,
502                } => self.handle_h3_data(
503                    cx,
504                    buf,
505                    remaining_in_frame,
506                    total,
507                    frame_type,
508                    partial_frame_header,
509                ),
510                ReceivedBodyState::ReadingH1Trailers { total } => {
511                    self.handle_reading_h1_trailers(cx, buf, total)
512                }
513                End => Ready(Ok((End, 0))),
514            })?;
515
516            *self.state = new_body_state;
517
518            if *self.state == End {
519                if bytes == 0
520                    && let Some(h3_trailer_future) = self.h3_trailer_future.as_mut()
521                {
522                    let trailers = ready!(h3_trailer_future.as_mut().poll(cx))?;
523                    *self.trailers = Some(trailers);
524                    *self.h3_trailer_future = None;
525                }
526
527                // h2 trailers are stashed on the per-stream `StreamState` before EOF, so
528                // they're already present at `End` — no boxed future needed. Replacing
529                // the session with `Http1` makes subsequent `End` re-entries idempotent.
530                if bytes == 0
531                    && let Some((h2_connection, stream_id)) =
532                        std::mem::replace(&mut self.protocol_session, ProtocolSession::Http1)
533                            .as_h2()
534                    && let Some(trailers) = h2_connection.take_trailers(stream_id)
535                {
536                    *self.trailers = Some(trailers);
537                }
538
539                if self.on_completion.is_some() && self.owns_transport() {
540                    let transport = self.transport.take().unwrap().unwrap_owned();
541                    let on_completion = self.on_completion.take().unwrap();
542                    on_completion(transport);
543                }
544                return Ready(Ok(bytes));
545            } else if bytes != 0 {
546                return Ready(Ok(bytes));
547            }
548        }
549
550        cx.waker().wake_by_ref();
551        Pending
552    }
553}
554
555impl<Transport> crate::BodySource for ReceivedBody<'static, Transport>
556where
557    Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
558{
559    fn trailers(self: Pin<&mut Self>) -> Option<Headers> {
560        self.get_mut().trailers.take()
561    }
562}
563
564impl<Transport> Debug for ReceivedBody<'_, Transport> {
565    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
566        f.debug_struct("ReceivedBody")
567            .field("state", &*self.state)
568            .field("content_length", &self.content_length)
569            .field("buffer", &format_args!(".."))
570            .field("on_completion", &self.on_completion.is_some())
571            .finish()
572    }
573}
574
575/// The type of H3 frame currently being processed in [`ReceivedBodyState::H3Data`].
576#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
577#[allow(missing_docs)]
578#[doc(hidden)]
579pub enum H3BodyFrameType {
580    /// Initial state — no frame decoded yet.
581    #[default]
582    Start,
583    /// Inside a DATA frame — body bytes to keep.
584    Data,
585    /// Inside an unknown frame — payload bytes to discard.
586    Unknown,
587    /// Inside a trailing HEADERS frame — accumulate into buffer for parsing.
588    Trailers,
589}
590
591#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
592#[allow(missing_docs)]
593#[doc(hidden)]
594pub enum ReceivedBodyState {
595    /// read state for a chunked-encoded body. the number of bytes that have been read from the
596    /// current chunk is the difference between remaining and total.
597    Chunked {
598        /// remaining indicates the bytes left _in the current
599        /// chunk_. initial state is zero.
600        remaining: u64,
601
602        /// total indicates the absolute number of bytes read from all chunks
603        total: u64,
604    },
605
606    /// read state when we have buffered content between subsequent polls because chunk framing
607    /// overlapped a buffer boundary
608    PartialChunkSize { total: u64 },
609
610    /// Plain payload bytes from the transport, with optional content-length bound. No
611    /// framing happens here, just `max_len` / content-length enforcement against a
612    /// running total. With a declared length, reads are clamped to the remaining
613    /// declared bytes and the state ends at the boundary; without one, ends on EOF.
614    ///
615    /// Used for HTTP/1.x bodies declared via `Content-Length`, HTTP/2 bodies (the h2
616    /// driver demuxes DATA frames into a per-stream receive ring upstream of this),
617    /// HTTP/1.0 read-to-close response bodies, and raw upgrade transports (CONNECT,
618    /// websockets-over-h1).
619    Raw {
620        /// total body bytes read.
621        total: u64,
622    },
623
624    /// read state for an H3 body framed as DATA frames.
625    H3Data {
626        /// bytes remaining in the current frame (DATA, Unknown, or Trailers). zero means we need
627        /// to read the next frame header.
628        remaining_in_frame: u64,
629
630        /// total body bytes read across all DATA frames.
631        total: u64,
632
633        /// what kind of frame we're currently inside.
634        frame_type: H3BodyFrameType,
635
636        /// when true, a partial frame header is sitting in `self.buffer` and needs more bytes
637        /// before we can decode it.
638        partial_frame_header: bool,
639    },
640
641    /// accumulating the HTTP/1.1 chunked trailer-section after the last-chunk (`0\r\n`).
642    ///
643    /// The trailer bytes (including any partially-received trailer headers) live in
644    /// `ReceivedBody::buffer` until a final empty line (`\r\n\r\n` or bare `\r\n`) is found.
645    /// Accumulation is capped at `HttpConfig::max_header_list_size` — the same bound the
646    /// h2/h3 field-section decoders apply — and errors out past it.
647    ReadingH1Trailers {
648        /// total body bytes read across all chunks (for bounds-checking)
649        total: u64,
650    },
651
652    /// the terminal read state
653    #[default]
654    End,
655}
656
657impl ReceivedBodyState {
658    /// Initial state for an HTTP/1.x body framed via `Content-Length` and/or
659    /// `Transfer-Encoding: chunked`. Chunked encoding produces [`Self::Chunked`];
660    /// `Some(0)` collapses to [`Self::End`]; everything else — including `None` for
661    /// HTTP/1.0 read-to-close — produces [`Self::Raw`], whose handler clamps reads to
662    /// the declared length when one is present.
663    pub fn new_h1(content_length: Option<u64>, transfer_encoding_chunked: bool) -> Self {
664        if transfer_encoding_chunked {
665            Self::Chunked {
666                remaining: 0,
667                total: 0,
668            }
669        } else if let Some(0) = content_length {
670            Self::End
671        } else {
672            Self::Raw { total: 0 }
673        }
674    }
675
676    /// Initial state for an HTTP/2 body — [`Self::Raw`] with a zero running total,
677    /// since the h2 transport already yields plain payload bytes.
678    pub fn new_h2() -> Self {
679        Self::Raw { total: 0 }
680    }
681
682    /// Initial state for an HTTP/3 body framed as DATA frames.
683    pub fn new_h3() -> Self {
684        Self::H3Data {
685            remaining_in_frame: 0,
686            total: 0,
687            frame_type: H3BodyFrameType::Start,
688            partial_frame_header: false,
689        }
690    }
691
692    /// Whether the body's read state is one whose first poll has not yet produced any
693    /// bytes. False for [`Self::End`] (terminal) and for the intermediate states
694    /// [`Self::PartialChunkSize`] / [`Self::ReadingH1Trailers`] that are only reached
695    /// after some reading has occurred.
696    pub fn is_unread(&self) -> bool {
697        matches!(
698            self,
699            Self::Chunked {
700                total: 0,
701                remaining: 0
702            } | Self::Raw { total: 0 }
703                | Self::H3Data { total: 0, .. }
704        )
705    }
706}
707
708impl<Transport> From<ReceivedBody<'static, Transport>> for Body
709where
710    Transport: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
711{
712    fn from(rb: ReceivedBody<'static, Transport>) -> Self {
713        let len = rb.content_length;
714        Body::new_with_trailers(rb, len)
715    }
716}