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 /// Retype as `ReceivedBody<'static, T>` if every internal `MutCow` field is `Owned`.
359 ///
360 /// Returns `None` if any field is `Borrowed`, in which case `self` is dropped — the
361 /// borrows can't be extended, and there's no useful way to hand a half-destructured
362 /// body back. For callers whose runtime invariants guarantee ownership but whose
363 /// type-level `'a` parameter the compiler can't see is `'static`.
364 #[doc(hidden)]
365 #[cfg(feature = "unstable")]
366 pub fn try_into_owned(self) -> Option<ReceivedBody<'static, T>> {
367 let Self {
368 content_length,
369 buffer,
370 transport,
371 state,
372 on_completion,
373 encoding,
374 max_len,
375 initial_len,
376 copy_loops_per_yield,
377 max_preallocate,
378 max_header_list_size,
379 trailers,
380 send_100_continue_offset,
381 protocol_session,
382 h3_trailer_future,
383 h3_trailer_payload_buffer,
384 } = self;
385
386 let transport = match transport {
387 None => None,
388 Some(t) => Some(t.try_into_owned()?),
389 };
390
391 Some(ReceivedBody {
392 content_length,
393 buffer: buffer.try_into_owned()?,
394 transport,
395 state: state.try_into_owned()?,
396 on_completion,
397 encoding,
398 max_len,
399 initial_len,
400 copy_loops_per_yield,
401 max_preallocate,
402 max_header_list_size,
403 trailers: trailers.try_into_owned()?,
404 send_100_continue_offset,
405 protocol_session,
406 h3_trailer_future: h3_trailer_future.try_into_owned()?,
407 h3_trailer_payload_buffer: h3_trailer_payload_buffer.try_into_owned()?,
408 })
409 }
410}
411
412pub(crate) fn read_buffered<Transport>(
413 buffer: &mut Buffer,
414 transport: &mut Transport,
415 cx: &mut Context<'_>,
416 buf: &mut [u8],
417) -> Poll<io::Result<usize>>
418where
419 Transport: AsyncRead + Unpin,
420{
421 if buffer.is_empty() {
422 Pin::new(transport).poll_read(cx, buf)
423 } else if buffer.len() >= buf.len() {
424 let len = buf.len();
425 buf.copy_from_slice(&buffer[..len]);
426 buffer.ignore_front(len);
427 Ready(Ok(len))
428 } else {
429 let self_buffer_len = buffer.len();
430 buf[..self_buffer_len].copy_from_slice(buffer);
431 buffer.truncate(0);
432 match Pin::new(transport).poll_read(cx, &mut buf[self_buffer_len..]) {
433 Ready(Ok(additional)) => Ready(Ok(additional + self_buffer_len)),
434 Pending => Ready(Ok(self_buffer_len)),
435 other @ Ready(_) => other,
436 }
437 }
438}
439
440type StateOutput = Poll<io::Result<(ReceivedBodyState, usize)>>;
441
442impl<Transport> AsyncRead for ReceivedBody<'_, Transport>
443where
444 Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
445{
446 fn poll_read(
447 mut self: Pin<&mut Self>,
448 cx: &mut Context<'_>,
449 buf: &mut [u8],
450 ) -> Poll<io::Result<usize>> {
451 const CONTINUE: &[u8] = b"HTTP/1.1 100 Continue\r\n\r\n";
452 while let Some(offset) = self.send_100_continue_offset {
453 let n = {
454 let Some(transport) = self.transport.as_deref_mut() else {
455 return Ready(Err(ErrorKind::NotConnected.into()));
456 };
457 if offset == 0 {
458 log::trace!("sending 100-continue");
459 }
460 ready!(Pin::new(transport).poll_write(cx, &CONTINUE[offset..]))?
461 };
462 if n == 0 {
463 return Ready(Err(ErrorKind::WriteZero.into()));
464 }
465 let new_offset = offset + n;
466 self.send_100_continue_offset = if new_offset >= CONTINUE.len() {
467 None
468 } else {
469 Some(new_offset)
470 };
471 }
472
473 for _ in 0..self.copy_loops_per_yield {
474 let (new_body_state, bytes) = ready!(match *self.state {
475 Chunked { remaining, total } => self.handle_chunked(cx, buf, remaining, total),
476 PartialChunkSize { total } => self.handle_partial(cx, buf, total),
477 Raw { total } => self.handle_raw(cx, buf, total),
478 ReceivedBodyState::H3Data {
479 remaining_in_frame,
480 total,
481 frame_type,
482 partial_frame_header,
483 } => self.handle_h3_data(
484 cx,
485 buf,
486 remaining_in_frame,
487 total,
488 frame_type,
489 partial_frame_header,
490 ),
491 ReceivedBodyState::ReadingH1Trailers { total } => {
492 self.handle_reading_h1_trailers(cx, buf, total)
493 }
494 End => Ready(Ok((End, 0))),
495 })?;
496
497 *self.state = new_body_state;
498
499 if *self.state == End {
500 if bytes == 0
501 && let Some(h3_trailer_future) = self.h3_trailer_future.as_mut()
502 {
503 let trailers = ready!(h3_trailer_future.as_mut().poll(cx))?;
504 *self.trailers = Some(trailers);
505 *self.h3_trailer_future = None;
506 }
507
508 // h2 trailers are stashed on the per-stream `StreamState` before EOF, so
509 // they're already present at `End` — no boxed future needed. Replacing
510 // the session with `Http1` makes subsequent `End` re-entries idempotent.
511 if bytes == 0
512 && let Some((h2_connection, stream_id)) =
513 std::mem::replace(&mut self.protocol_session, ProtocolSession::Http1)
514 .as_h2()
515 && let Some(trailers) = h2_connection.take_trailers(stream_id)
516 {
517 *self.trailers = Some(trailers);
518 }
519
520 if self.on_completion.is_some() && self.owns_transport() {
521 let transport = self.transport.take().unwrap().unwrap_owned();
522 let on_completion = self.on_completion.take().unwrap();
523 on_completion(transport);
524 }
525 return Ready(Ok(bytes));
526 } else if bytes != 0 {
527 return Ready(Ok(bytes));
528 }
529 }
530
531 cx.waker().wake_by_ref();
532 Pending
533 }
534}
535
536impl<Transport> crate::BodySource for ReceivedBody<'static, Transport>
537where
538 Transport: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
539{
540 fn trailers(self: Pin<&mut Self>) -> Option<Headers> {
541 self.get_mut().trailers.take()
542 }
543}
544
545impl<Transport> Debug for ReceivedBody<'_, Transport> {
546 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
547 f.debug_struct("ReceivedBody")
548 .field("state", &*self.state)
549 .field("content_length", &self.content_length)
550 .field("buffer", &format_args!(".."))
551 .field("on_completion", &self.on_completion.is_some())
552 .finish()
553 }
554}
555
556/// The type of H3 frame currently being processed in [`ReceivedBodyState::H3Data`].
557#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
558#[allow(missing_docs)]
559#[doc(hidden)]
560pub enum H3BodyFrameType {
561 /// Initial state — no frame decoded yet.
562 #[default]
563 Start,
564 /// Inside a DATA frame — body bytes to keep.
565 Data,
566 /// Inside an unknown frame — payload bytes to discard.
567 Unknown,
568 /// Inside a trailing HEADERS frame — accumulate into buffer for parsing.
569 Trailers,
570}
571
572#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
573#[allow(missing_docs)]
574#[doc(hidden)]
575pub enum ReceivedBodyState {
576 /// read state for a chunked-encoded body. the number of bytes that have been read from the
577 /// current chunk is the difference between remaining and total.
578 Chunked {
579 /// remaining indicates the bytes left _in the current
580 /// chunk_. initial state is zero.
581 remaining: u64,
582
583 /// total indicates the absolute number of bytes read from all chunks
584 total: u64,
585 },
586
587 /// read state when we have buffered content between subsequent polls because chunk framing
588 /// overlapped a buffer boundary
589 PartialChunkSize { total: u64 },
590
591 /// Plain payload bytes from the transport, with optional content-length bound. No
592 /// framing happens here, just `max_len` / content-length enforcement against a
593 /// running total. With a declared length, reads are clamped to the remaining
594 /// declared bytes and the state ends at the boundary; without one, ends on EOF.
595 ///
596 /// Used for HTTP/1.x bodies declared via `Content-Length`, HTTP/2 bodies (the h2
597 /// driver demuxes DATA frames into a per-stream receive ring upstream of this),
598 /// HTTP/1.0 read-to-close response bodies, and raw upgrade transports (CONNECT,
599 /// websockets-over-h1).
600 Raw {
601 /// total body bytes read.
602 total: u64,
603 },
604
605 /// read state for an H3 body framed as DATA frames.
606 H3Data {
607 /// bytes remaining in the current frame (DATA, Unknown, or Trailers). zero means we need
608 /// to read the next frame header.
609 remaining_in_frame: u64,
610
611 /// total body bytes read across all DATA frames.
612 total: u64,
613
614 /// what kind of frame we're currently inside.
615 frame_type: H3BodyFrameType,
616
617 /// when true, a partial frame header is sitting in `self.buffer` and needs more bytes
618 /// before we can decode it.
619 partial_frame_header: bool,
620 },
621
622 /// accumulating the HTTP/1.1 chunked trailer-section after the last-chunk (`0\r\n`).
623 ///
624 /// The trailer bytes (including any partially-received trailer headers) live in
625 /// `ReceivedBody::buffer` until a final empty line (`\r\n\r\n` or bare `\r\n`) is found.
626 ReadingH1Trailers {
627 /// total body bytes read across all chunks (for bounds-checking)
628 total: u64,
629 },
630
631 /// the terminal read state
632 #[default]
633 End,
634}
635
636impl ReceivedBodyState {
637 /// Initial state for an HTTP/1.x body framed via `Content-Length` and/or
638 /// `Transfer-Encoding: chunked`. Chunked encoding produces [`Self::Chunked`];
639 /// `Some(0)` collapses to [`Self::End`]; everything else — including `None` for
640 /// HTTP/1.0 read-to-close — produces [`Self::Raw`], whose handler clamps reads to
641 /// the declared length when one is present.
642 pub fn new_h1(content_length: Option<u64>, transfer_encoding_chunked: bool) -> Self {
643 if transfer_encoding_chunked {
644 Self::Chunked {
645 remaining: 0,
646 total: 0,
647 }
648 } else if let Some(0) = content_length {
649 Self::End
650 } else {
651 Self::Raw { total: 0 }
652 }
653 }
654
655 /// Initial state for an HTTP/2 body — [`Self::Raw`] with a zero running total,
656 /// since the h2 transport already yields plain payload bytes.
657 pub fn new_h2() -> Self {
658 Self::Raw { total: 0 }
659 }
660
661 /// Initial state for an HTTP/3 body framed as DATA frames.
662 pub fn new_h3() -> Self {
663 Self::H3Data {
664 remaining_in_frame: 0,
665 total: 0,
666 frame_type: H3BodyFrameType::Start,
667 partial_frame_header: false,
668 }
669 }
670
671 /// Whether the body's read state is one whose first poll has not yet produced any
672 /// bytes. False for [`Self::End`] (terminal) and for the intermediate states
673 /// [`Self::PartialChunkSize`] / [`Self::ReadingH1Trailers`] that are only reached
674 /// after some reading has occurred.
675 pub fn is_unread(&self) -> bool {
676 matches!(
677 self,
678 Self::Chunked {
679 total: 0,
680 remaining: 0
681 } | Self::Raw { total: 0 }
682 | Self::H3Data { total: 0, .. }
683 )
684 }
685}
686
687impl<Transport> From<ReceivedBody<'static, Transport>> for Body
688where
689 Transport: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
690{
691 fn from(rb: ReceivedBody<'static, Transport>) -> Self {
692 let len = rb.content_length;
693 Body::new_with_trailers(rb, len)
694 }
695}