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}