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