Skip to main content

trillium_client/
response_body.rs

1use crate::{Error, Pool, pool::PoolEntry};
2use encoding_rs::Encoding;
3use futures_lite::{AsyncRead, AsyncReadExt, AsyncWriteExt};
4use std::{
5    fmt::{self, Debug, Formatter},
6    io, mem,
7    pin::{Pin, pin},
8    sync::{
9        Arc,
10        atomic::{AtomicBool, Ordering},
11    },
12    task::{Context, Poll, Wake, Waker, ready},
13    time::{Duration, Instant},
14};
15use trillium_http::{
16    Body, BodySource, Headers, HttpConfig, MutCow, ReceivedBody, ReceivedBodyState,
17};
18use trillium_server_common::{Runtime, Transport, url::Origin};
19
20/// A response body received from a server.
21///
22/// Most of the time this represents a body that will be read from the underlying transport, but it
23/// can also wrap an override body installed by middleware via [`ConnExt::set_response_body`]
24/// — e.g. cache hits, mocked responses, or circuit-breaker short-circuits. Reads, encoding
25/// handling, and `max_len` enforcement work transparently across both cases.
26///
27/// [`ConnExt::set_response_body`]: crate::ConnExt::set_response_body
28///
29/// ```rust
30/// use trillium_client::Client;
31/// use trillium_testing::{client_config, with_server};
32///
33/// with_server("hello from trillium", |url| async move {
34///     let client = Client::new(client_config());
35///     let mut conn = client.get(url).await?;
36///     let body = conn.response_body(); //<-
37///     assert_eq!(Some(19), body.content_length());
38///     assert_eq!("hello from trillium", body.read_string().await?);
39///     Ok(())
40/// });
41/// ```
42///
43/// ## Bounds checking
44///
45/// Every `ResponseBody` has a maximum length beyond which it will return an error, expressed as a
46/// u64. To override this on the specific `ResponseBody`, use [`ResponseBody::with_max_len`] or
47/// [`ResponseBody::set_max_len`]. The bound is enforced on override bodies as well as
48/// transport-backed ones, so a user-set memory cap holds even when middleware has replaced the
49/// body with externally-sourced bytes.
50pub struct ResponseBody<'a> {
51    inner: ResponseBodyInner<'a>,
52    /// Set on `'static` Received bodies built via
53    /// [`Conn::take_response_body`][crate::Conn::take_response_body]. `recycle` and `Drop`
54    /// consult it to decide whether to drain (keepalive) or close the underlying transport.
55    /// `None` for borrowed bodies (the conn handles their cleanup) and for Override bodies (no
56    /// transport is attached at this layer — `take_response_body` already evicted any leftover
57    /// transport before returning).
58    cleanup: Option<CleanupContext>,
59    /// Trailers harvested off the inner [`ReceivedBody`] when it reaches `End`. The
60    /// EOF-driven recycle in `poll_read` moves the `ReceivedBody` out before the caller can
61    /// observe its trailers, so they're captured here to outlive it and surfaced through
62    /// [`BodySource::trailers`].
63    trailers: Option<Headers>,
64}
65
66#[allow(clippy::large_enum_variant)]
67enum ResponseBodyInner<'a> {
68    Received(ReceivedBody<'a, Box<dyn Transport>>),
69    Override(OverrideBody<'a>),
70    Closing(Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>),
71    Closed,
72}
73
74type H1Pool = Pool<Origin, Box<dyn Transport>>;
75
76/// Carries everything `Drop for ResponseBody` and [`ResponseBody::recycle`] need to release
77/// a transport without re-deriving from a [`crate::Conn`] (which is gone by then).
78///
79/// `pool_origin: Some` means "keepalive transport, pool is configured — insert here on
80/// completion." `None` means "close on completion" (non-keepalive *or* no pool). The same
81/// instance is cloned into the body's `on_completion` callback in
82/// [`Conn::take_received_body`][crate::Conn::take_received_body], so the user-driven
83/// drain/read paths and the Drop/recycle paths share one source of truth for what to do
84/// with the transport when the body finishes.
85#[derive(Clone)]
86pub(crate) struct CleanupContext {
87    pub(crate) runtime: Runtime,
88    pub(crate) h1_pool_origin: Option<(H1Pool, Origin)>,
89    /// Idle lifetime stamped onto a pooled h1 transport, counted from the moment it returns to
90    /// the pool. `None` disables expiry. Sourced from
91    /// [`Client::h1_idle_timeout`][crate::Client::h1_idle_timeout].
92    pub(crate) h1_idle_timeout: Option<Duration>,
93}
94
95/// The expiry [`Instant`] to stamp on an h1 [`PoolEntry`] returning to the pool now, given the
96/// configured idle timeout — i.e. `now + timeout`, or `None` when expiry is disabled.
97fn pool_expiry(h1_idle_timeout: Option<Duration>) -> Option<Instant> {
98    h1_idle_timeout.map(|d| Instant::now() + d)
99}
100
101impl CleanupContext {
102    /// Hand a freshly-released transport off to its destination — pool insert (sync) or
103    /// spawn close.
104    pub(crate) fn handoff(&self, mut transport: Box<dyn Transport>) {
105        match &self.h1_pool_origin {
106            Some((pool, origin)) => {
107                log::trace!("body transferred, returning to pool");
108                pool.insert(
109                    origin.clone(),
110                    PoolEntry::new(transport, pool_expiry(self.h1_idle_timeout)),
111                );
112            }
113            None => {
114                self.runtime.clone().spawn(async move {
115                    log_close_result(transport.close().await);
116                });
117            }
118        }
119    }
120}
121
122pub(crate) struct OverrideBody<'a> {
123    body: MutCow<'a, Body>,
124    encoding: &'static Encoding,
125    max_len: u64,
126    initial_len: usize,
127    max_preallocate: usize,
128}
129
130impl AsyncRead for OverrideBody<'_> {
131    fn poll_read(
132        mut self: Pin<&mut Self>,
133        cx: &mut Context<'_>,
134        buf: &mut [u8],
135    ) -> Poll<io::Result<usize>> {
136        let remaining = self.max_len.saturating_sub(self.body.bytes_read());
137        if remaining == 0 && !buf.is_empty() {
138            return Poll::Ready(Err(io::Error::other(Error::ReceivedBodyTooLong(
139                self.max_len,
140            ))));
141        }
142        let cap = remaining.min(buf.len() as u64) as usize;
143        Pin::new(&mut *self.body).poll_read(cx, &mut buf[..cap])
144    }
145}
146
147impl<'a> OverrideBody<'a> {
148    pub(crate) fn new(
149        body: impl Into<MutCow<'a, Body>>,
150        encoding: &'static Encoding,
151        http_config: &HttpConfig,
152    ) -> Self {
153        Self {
154            body: body.into(),
155            encoding,
156            max_len: http_config.received_body_max_len(),
157            max_preallocate: http_config.received_body_max_preallocate(),
158            initial_len: http_config.received_body_initial_len(),
159        }
160    }
161}
162
163impl Debug for ResponseBody<'_> {
164    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
165        match &self.inner {
166            ResponseBodyInner::Received(rb) => f.debug_tuple("ResponseBody").field(rb).finish(),
167            ResponseBodyInner::Override(o) => f
168                .debug_struct("ResponseBody (override)")
169                .field("body", &*o.body)
170                .field("encoding", &o.encoding.name())
171                .field("max_len", &o.max_len)
172                .finish(),
173            ResponseBodyInner::Closing(_) => f.debug_tuple("ResponseBody (closing)").finish(),
174            ResponseBodyInner::Closed => f.debug_tuple("ResponseBody (closed)").finish(),
175        }
176    }
177}
178
179impl AsyncRead for ResponseBody<'_> {
180    fn poll_read(
181        mut self: Pin<&mut Self>,
182        cx: &mut Context<'_>,
183        buf: &mut [u8],
184    ) -> Poll<io::Result<usize>> {
185        let mut bytes = 0;
186        loop {
187            match &mut self.inner {
188                ResponseBodyInner::Received(rb) => bytes = ready!(Pin::new(rb).poll_read(cx, buf))?,
189                ResponseBodyInner::Override(o) => bytes = ready!(Pin::new(o).poll_read(cx, buf))?,
190                ResponseBodyInner::Closing(fut) => {
191                    ready!(fut.as_mut().poll(cx));
192                    self.inner = ResponseBodyInner::Closed;
193                    break;
194                }
195
196                ResponseBodyInner::Closed => break,
197            };
198
199            // Inline transport settlement — see take_received_body's `cleanup` param for
200            // why this isn't done via on_completion.
201            if bytes == 0
202                && let Some((mut rb, cleanup)) = self.prepare_for_recycle()
203                && rb.state() == ReceivedBodyState::End
204                && let Some(mut transport) = rb.take_transport()
205            {
206                self.trailers = Pin::new(&mut rb).trailers();
207                if let Some((pool, origin)) = cleanup.h1_pool_origin {
208                    pool.insert(
209                        origin,
210                        PoolEntry::new(transport, pool_expiry(cleanup.h1_idle_timeout)),
211                    );
212                } else {
213                    self.inner = ResponseBodyInner::Closing(Box::pin(async move {
214                        log_close_result(transport.close().await);
215                    }));
216                }
217            } else {
218                break;
219            }
220        }
221
222        Poll::Ready(Ok(bytes))
223    }
224}
225
226impl ResponseBody<'_> {
227    fn take_inner(&mut self) -> ResponseBodyInner<'_> {
228        mem::replace(&mut self.inner, ResponseBodyInner::Closed)
229    }
230
231    fn max_preallocate(&self) -> usize {
232        match &self.inner {
233            ResponseBodyInner::Received(rb) => rb.max_preallocate(),
234            ResponseBodyInner::Override(override_body) => override_body.max_preallocate,
235            _ => 0,
236        }
237    }
238
239    fn max_len(&self) -> u64 {
240        match &self.inner {
241            ResponseBodyInner::Received(rb) => rb.max_len(),
242            ResponseBodyInner::Override(override_body) => override_body.max_len,
243            _ => 0,
244        }
245    }
246
247    fn initial_len(&self) -> usize {
248        match &self.inner {
249            ResponseBodyInner::Received(rb) => rb.initial_len(),
250            ResponseBodyInner::Override(override_body) => override_body.initial_len,
251            _ => 0,
252        }
253    }
254
255    fn encoding(&self) -> &'static Encoding {
256        match &self.inner {
257            ResponseBodyInner::Received(rb) => rb.encoding(),
258            ResponseBodyInner::Override(override_body) => override_body.encoding,
259            _ => encoding_rs::UTF_8,
260        }
261    }
262
263    /// Similar to [`ResponseBody::read_string`], but returns the raw bytes. This is useful for
264    /// bodies that are not text.
265    ///
266    /// You can use this in conjunction with `encoding` if you need different handling of malformed
267    /// character encoding than the lossy conversion provided by [`ResponseBody::read_string`].
268    ///
269    /// An empty or nonexistent body will yield an empty Vec, not an error.
270    ///
271    /// # Errors
272    ///
273    /// This will return an error if there is an IO error on the underlying transport such as a
274    /// disconnect.
275    ///
276    /// This will also return an error if the length exceeds the maximum length. To configure the
277    /// value on this specific request body, use [`ResponseBody::with_max_len`] or
278    /// [`ResponseBody::set_max_len`]
279    pub async fn read_bytes(mut self) -> Result<Vec<u8>, Error> {
280        let mut vec = if let Some(len) = self.content_length() {
281            if len > self.max_len() {
282                return Err(Error::ReceivedBodyTooLong(self.max_len()));
283            }
284
285            let len =
286                usize::try_from(len).map_err(|_| Error::ReceivedBodyTooLong(self.max_len()))?;
287
288            Vec::with_capacity(len.min(self.max_preallocate()))
289        } else {
290            Vec::with_capacity(self.initial_len())
291        };
292
293        self.read_to_end(&mut vec).await?;
294
295        Ok(vec)
296    }
297
298    /// Reads the entire body to a `String`.
299    ///
300    /// Uses the encoding determined by the content-type (mime) charset. If an encoding problem
301    /// is encountered, the returned `String` will contain utf8 replacement characters.
302    ///
303    /// Note that this can only be performed once per Conn, as the underlying data is not cached
304    /// anywhere. This is the only copy of the body contents.
305    ///
306    /// An empty or nonexistent body will yield an empty String, not an error
307    ///
308    /// # Errors
309    ///
310    /// This will return an error if there is an IO error on the
311    /// underlying transport such as a disconnect
312    ///
313    ///
314    /// This will also return an error if the length exceeds the maximum length. To configure the
315    /// value on this specific response body, use [`ResponseBody::with_max_len`] or
316    /// [`ResponseBody::set_max_len`].
317    pub async fn read_string(self) -> Result<String, Error> {
318        let encoding = self.encoding();
319        let bytes = self.read_bytes().await?;
320        let (s, _, _) = encoding.decode(&bytes);
321        Ok(s.to_string())
322    }
323
324    /// Set the maximum content length to read, returning self
325    ///
326    /// This protects against a memory-use denial-of-service attack wherein an untrusted peer sends
327    /// an unbounded request body. This is especially important when using
328    /// [`ResponseBody::read_string`] and [`ResponseBody::read_bytes`] instead of streaming with
329    /// `AsyncRead`.
330    ///
331    /// The default value can be found documented [in the trillium-http
332    /// crate](https://docs.trillium.rs/trillium_http/struct.HttpConfig#method.received_body_max_len)
333    #[must_use]
334    pub fn with_max_len(mut self, max_len: u64) -> Self {
335        self.set_max_len(max_len);
336        self
337    }
338
339    /// Set the maximum content length to read
340    ///
341    /// This protects against a memory-use denial-of-service attack wherein an untrusted peer sends
342    /// an unbounded request body. This is especially important when using
343    /// [`ResponseBody::read_string`] and [`ResponseBody::read_bytes`] instead of streaming with
344    /// `AsyncRead`.
345    ///
346    /// The default value can be found documented [in the trillium-http
347    /// crate](https://docs.trillium.rs/trillium_http/struct.HttpConfig#method.received_body_max_len)
348    pub fn set_max_len(&mut self, max_len: u64) -> &mut Self {
349        match &mut self.inner {
350            ResponseBodyInner::Received(rb) => {
351                rb.set_max_len(max_len);
352            }
353            ResponseBodyInner::Override(o) => {
354                o.max_len = max_len;
355            }
356            _ => {}
357        }
358        self
359    }
360
361    /// The trailers received after the response body, if any.
362    ///
363    /// Returns `None` until the body has been read to end-of-stream, and only on protocols
364    /// that delivered a trailer section (HTTP/1.1 chunked with trailers, HTTP/2, HTTP/3).
365    /// Reading the body via [`read_string`](Self::read_string)/[`read_bytes`](Self::read_bytes)
366    /// consumes it, so to observe trailers drive the body to completion through its
367    /// [`AsyncRead`](futures_lite::AsyncRead) interface and then call this.
368    pub fn trailers(&self) -> Option<&Headers> {
369        match &self.inner {
370            ResponseBodyInner::Received(rb) => rb.trailers_ref(),
371            // Captured off the inner ReceivedBody when it was recycled at end-of-stream.
372            _ => self.trailers.as_ref(),
373        }
374    }
375
376    /// The content-length of this body, if available.
377    ///
378    /// Usually derived from the content-length header. If the response uses
379    /// transfer-encoding chunked, this will be `None`.
380    pub fn content_length(&self) -> Option<u64> {
381        match &self.inner {
382            ResponseBodyInner::Received(rb) => rb.content_length(),
383            ResponseBodyInner::Override(o) => o.body.len(),
384            _ => None,
385        }
386    }
387
388    fn prepare_for_recycle(
389        &mut self,
390    ) -> Option<(
391        ReceivedBody<'static, Box<dyn Transport + 'static>>,
392        CleanupContext,
393    )> {
394        let cleanup = self.cleanup.take()?;
395
396        let ResponseBodyInner::Received(rb) = self.take_inner() else {
397            return None;
398        };
399
400        let rb = rb.try_into_owned()?;
401
402        Some((rb, cleanup))
403    }
404}
405
406async fn drain(rb: &mut ReceivedBody<'static, Box<dyn Transport + 'static>>) -> io::Result<u64> {
407    let copy_loops_per_yield = rb.copy_loops_per_yield();
408    trillium_http::copy(rb, futures_lite::io::sink(), copy_loops_per_yield).await
409}
410
411/// Best-effort synchronous drain of whatever body remainder is already buffered.
412///
413/// Small responses are often slurped in full during head parsing, so reaching
414/// `End` frequently requires no transport IO at all — just consuming buffered
415/// bytes. This drives [`drain`] with a wake-flag waker: cooperative yields
416/// re-poll immediately, and the first genuine IO `Pending` (or a drain error)
417/// stops the attempt, leaving the body wherever it got to for an async caller
418/// to finish. Never parks and never performs IO waits, so it is safe to call
419/// from `Drop` in any context.
420fn drain_buffered(rb: &mut ReceivedBody<'static, Box<dyn Transport + 'static>>) {
421    struct WakeFlag(AtomicBool);
422    impl Wake for WakeFlag {
423        fn wake(self: Arc<Self>) {
424            self.wake_by_ref();
425        }
426
427        fn wake_by_ref(self: &Arc<Self>) {
428            self.0.store(true, Ordering::Relaxed);
429        }
430    }
431
432    let flag = Arc::new(WakeFlag(AtomicBool::new(false)));
433    let waker = Waker::from(flag.clone());
434    let mut cx = Context::from_waker(&waker);
435    let mut drain = pin!(drain(rb));
436    loop {
437        match drain.as_mut().poll(&mut cx) {
438            Poll::Ready(_) => return,
439            Poll::Pending if flag.0.swap(false, Ordering::Relaxed) => {}
440            Poll::Pending => return,
441        }
442    }
443}
444
445/// Report the result of closing a transport we're discarding. `NotConnected` is the expected
446/// "already closed" signal from a finished multiplexed stream — notably h3/QUIC, whose `close`
447/// (unlike h2's) isn't idempotent and errors once the stream has finished — so it's absorbed at
448/// trace. Other errors are unexpected and warned.
449fn log_close_result(result: io::Result<()>) {
450    match result {
451        Ok(()) => {}
452        Err(e) if e.kind() == io::ErrorKind::NotConnected => {
453            log::trace!("transport already closed during recycle: {e}");
454        }
455        Err(e) => log::warn!("transport close failed during recycle: {e}"),
456    }
457}
458
459async fn recycle(
460    mut rb: ReceivedBody<'static, Box<dyn Transport + 'static>>,
461    h1_pool_origin: Option<(H1Pool, Origin)>,
462    h1_idle_timeout: Option<Duration>,
463) {
464    if let Some((pool, origin)) = h1_pool_origin {
465        match drain(&mut rb).await {
466            Ok(drained) => {
467                if rb.state() == ReceivedBodyState::End
468                    && let Some(transport) = rb.take_transport()
469                {
470                    log::trace!(
471                        "drained {drained} bytes, returning transport to pool for {origin:?}"
472                    );
473                    pool.insert(
474                        origin,
475                        PoolEntry::new(transport, pool_expiry(h1_idle_timeout)),
476                    );
477                    return;
478                }
479            }
480            Err(e) => log::warn!("drain failed during recycle: {e}"),
481        }
482    }
483
484    if let Some(mut transport) = rb.take_transport() {
485        log_close_result(transport.close().await);
486    }
487}
488
489impl Drop for ResponseBody<'_> {
490    fn drop(&mut self) {
491        let Some((mut rb, cleanup)) = self.prepare_for_recycle() else {
492            return;
493        };
494
495        // Sync fast path for reclaiming an owned http/1.1 keepalive body: consume any
496        // remainder that's already buffered, and if that reaches End, pool the transport
497        // before drop returns — so a subsequent request on this client deterministically
498        // reuses it. Bodies with bytes still on the wire fall through to a spawned drain
499        // (Drop can't await IO), as does the no-pool path (`transport.close()` is async).
500        if let Some((pool, origin)) = cleanup.h1_pool_origin {
501            if rb.state() != ReceivedBodyState::End {
502                drain_buffered(&mut rb);
503            }
504
505            if rb.state() == ReceivedBodyState::End
506                && let Some(transport) = rb.take_transport()
507            {
508                pool.insert(
509                    origin,
510                    PoolEntry::new(transport, pool_expiry(cleanup.h1_idle_timeout)),
511                );
512                return;
513            }
514
515            cleanup
516                .runtime
517                .spawn(recycle(rb, Some((pool, origin)), cleanup.h1_idle_timeout));
518        } else {
519            cleanup
520                .runtime
521                .spawn(recycle(rb, None, cleanup.h1_idle_timeout));
522        }
523    }
524}
525
526impl BodySource for ResponseBody<'static> {
527    fn trailers(self: Pin<&mut Self>) -> Option<Headers> {
528        let this = self.get_mut();
529        match &mut this.inner {
530            ResponseBodyInner::Received(rb) => Pin::new(rb).trailers(),
531            ResponseBodyInner::Override(o) => o.body.trailers(),
532            // Recycled at EOF — trailers were captured off the ReceivedBody before it was
533            // moved out. See `ResponseBody::trailers`.
534            _ => this.trailers.take(),
535        }
536    }
537}
538
539impl<'a> From<ReceivedBody<'a, Box<dyn Transport>>> for ResponseBody<'a> {
540    fn from(received_body: ReceivedBody<'a, Box<dyn Transport>>) -> Self {
541        Self {
542            inner: ResponseBodyInner::Received(received_body),
543            cleanup: None,
544            trailers: None,
545        }
546    }
547}
548
549impl<'a> From<OverrideBody<'a>> for ResponseBody<'a> {
550    fn from(o: OverrideBody<'a>) -> Self {
551        Self {
552            inner: ResponseBodyInner::Override(o),
553            cleanup: None,
554            trailers: None,
555        }
556    }
557}
558
559impl ResponseBody<'static> {
560    pub(crate) fn received_owned(
561        body: ReceivedBody<'static, Box<dyn Transport>>,
562        cleanup: CleanupContext,
563    ) -> Self {
564        Self {
565            inner: ResponseBodyInner::Received(body),
566            cleanup: Some(cleanup),
567            trailers: None,
568        }
569    }
570
571    /// Drains and pools the underlying transport when worthwhile, closes it otherwise.
572    ///
573    /// Use this to release a keepalive transport synchronously before reissuing a request on
574    /// the same client — the redirect/retry handler pattern. For an h1.1 keepalive transport
575    /// this drives the body to EOF and returns the transport to the pool. For a non-keepalive
576    /// transport this calls `transport.close()` directly without draining (since draining
577    /// would just waste bytes on a connection we're about to close).
578    ///
579    /// For an Override body (cache hit, mocked response, tee), this is a no-op — the body's
580    /// own components handle their own teardown when dropped.
581    pub async fn recycle(mut self) {
582        let Some((rb, cleanup)) = self.prepare_for_recycle() else {
583            return;
584        };
585
586        recycle(rb, cleanup.h1_pool_origin, cleanup.h1_idle_timeout).await;
587    }
588}
589
590impl<'a> IntoFuture for ResponseBody<'a> {
591    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
592    type Output = trillium_http::Result<String>;
593
594    fn into_future(self) -> Self::IntoFuture {
595        Box::pin(async move { self.read_string().await })
596    }
597}
598
599const _: fn() = || {
600    fn assert_send_sync<T: Send + Sync + ?Sized>() {}
601    assert_send_sync::<ResponseBody<'static>>();
602    assert_send_sync::<ResponseBody<'_>>();
603};