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