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}