Skip to main content

rama_http_backend/client/
bind_body.rs

1//! Tie a pooled connection's lifetime to the response body it produced.
2//!
3//! A connector returns its connection at response **headers** and the caller
4//! drops that handle immediately, but the connection is logically still in use
5//! until the response **body** has been read off the wire. For a pooled
6//! connection this matters: the pool would otherwise consider the connection
7//! free (reusable / idle) while a response body is still streaming.
8//!
9//! [`BindBodyToConn`] wraps such a connection and, on `serve`, moves it whole
10//! into the returned response body (via [`GuardedBody`]). The connection's own
11//! `Drop` (freeing a stream slot, returning it to the pool, …) then fires when
12//! the body reaches end-of-stream or is dropped, instead of at headers.
13//! [`BindBodyToConnLayer`] applies this to a connector's connection.
14//!
15//! Warning: this means a [`BindBodyToConn`] connection can only be used to
16//! serve a single request, which ties in perfects with how we expect a connector
17//! stack to work, but might be surprising if you use this layer alone. This layer
18//! should almost always be combined with connection pool that gives a new connection
19//! per request.
20
21use parking_lot::Mutex;
22
23use rama_core::bytes::Bytes;
24use rama_core::error::{BoxError, BoxErrorExt as _};
25use rama_core::extensions::{Extensions, ExtensionsRef};
26use rama_core::{Layer, Service};
27use rama_http::StreamingBody;
28use rama_http_types::body::GuardedBody;
29use rama_http_types::{Body, Request, Response};
30use rama_net::client::{ConnectionError, ConnectorService, EstablishedClientConnection};
31
32/// A connection wrapper that binds the connection to the lifetime of the
33/// response body it produces.
34///
35/// A connector returns its connection at response **headers**, but the
36/// connection is logically still in use until the response **body** has been
37/// read off the wire. On `serve`, this wrapper moves the connection whole into
38/// the returned response body (via [`GuardedBody`]), so the connection's `Drop`
39/// (freeing a stream slot, returning it to the pool, …) fires when the body
40/// reaches end-of-stream or is dropped, instead of at headers.
41///
42/// The connection is moved into the response body on `serve` (it is served
43/// exactly once per request), so there is no shared ownership of it.
44///
45/// Warning: the connection returned by this service should only be used for a single
46/// request, since it will be transferred to the response body and not be useable after that.
47pub struct BindBodyToConn<C> {
48    /// `Some` until the (single) `serve` moves the connection into the body.
49    conn: Mutex<Option<C>>,
50    /// Snapshot of the connection's extensions, taken at construction so that
51    /// connector layers above us (e.g. the request version adapter) and the
52    /// client can still read them after the connection has moved into the body.
53    /// Extension entries are `Arc`-backed, so live-updated ones (e.g. the peer's
54    /// max concurrency) are still observed through this clone.
55    extensions: Extensions,
56}
57
58impl<C: ExtensionsRef> BindBodyToConn<C> {
59    /// Wrap `conn` so its lifetime extends to the response body of its serve.
60    pub fn new(conn: C) -> Self {
61        Self {
62            extensions: conn.extensions().clone(),
63            conn: Mutex::new(Some(conn)),
64        }
65    }
66}
67
68impl<C> std::fmt::Debug for BindBodyToConn<C> {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("BindBodyToConn").finish_non_exhaustive()
71    }
72}
73
74impl<C> ExtensionsRef for BindBodyToConn<C> {
75    fn extensions(&self) -> &Extensions {
76        &self.extensions
77    }
78}
79
80impl<C, ReqBody, RespBody> Service<Request<ReqBody>> for BindBodyToConn<C>
81where
82    C: Service<Request<ReqBody>, Output = Response<RespBody>, Error = BoxError>,
83    ReqBody: Send + 'static,
84    RespBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
85{
86    type Output = Response;
87    type Error = BoxError;
88
89    async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
90        // Move the connection out: it is served exactly once per request, and it
91        // now lives for as long as its response body.
92        let Some(conn) = self.conn.lock().take() else {
93            return Err(BoxError::from_static_str(
94                "BindBodyToConn: connection already served (must be served exactly once)",
95            ));
96        };
97        let resp = conn.serve(req).await?;
98        // The connection is dropped, releasing its pool slot, when the body
99        // reaches end-of-stream or is dropped.
100        Ok(resp.map(|body| Body::new(GuardedBody::new(body, conn))))
101    }
102}
103
104/// [`Layer`] that wraps a connector's connection in [`BindBodyToConn`].
105#[derive(Debug, Clone, Default)]
106#[non_exhaustive]
107pub struct BindBodyToConnLayer;
108
109impl BindBodyToConnLayer {
110    /// Create a new [`BindBodyToConnLayer`].
111    #[must_use]
112    pub fn new() -> Self {
113        Self
114    }
115}
116
117impl<S> Layer<S> for BindBodyToConnLayer {
118    type Service = BindBodyToConnector<S>;
119
120    fn layer(&self, inner: S) -> Self::Service {
121        BindBodyToConnector { inner }
122    }
123}
124
125/// Connector produced by [`BindBodyToConnLayer`]: wraps each established
126/// connection in [`BindBodyToConn`].
127#[derive(Debug, Clone)]
128pub struct BindBodyToConnector<S> {
129    inner: S,
130}
131
132impl<S, Input> Service<Input> for BindBodyToConnector<S>
133where
134    S: ConnectorService<Input>,
135    Input: Send + 'static,
136{
137    type Output = EstablishedClientConnection<BindBodyToConn<S::Connection>, Input>;
138    type Error = ConnectionError;
139
140    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
141        let EstablishedClientConnection { conn, input } = self.inner.connect(input).await?;
142        Ok(EstablishedClientConnection {
143            conn: BindBodyToConn::new(conn),
144            input,
145        })
146    }
147}