Skip to main content

rig_http/http_client/
erased.rs

1//! Type-erased HTTP transport with optional middleware.
2//! Without middleware, requests pass through unchanged after body conversion to
3//! bytes; lazy response bodies convert through `U::from`.
4//!
5//! ```
6//! use rig_http::http_client::{DynHttpClient, HttpClientExt};
7//!
8//! fn erase(client: impl HttpClientExt + 'static) -> DynHttpClient {
9//!     DynHttpClient::new(client)
10//! }
11//! ```
12
13use std::any::Any;
14use std::fmt;
15use std::sync::Arc;
16
17use bytes::Bytes;
18use http::{Request, Response};
19
20use super::middleware::HttpMiddleware;
21use super::{HttpClientExt, LazyBody, MultipartForm, Result, StreamingResponse};
22use crate::wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync};
23
24/// Object-safe mirror of [`HttpClientExt`] with the generics fixed to
25/// [`Bytes`]. Private: the only way to reach it is through
26/// [`DynHttpClient`], which re-exposes the generic surface.
27pub(crate) trait ErasedHttpClient: WasmCompatSend + WasmCompatSync {
28    fn send_bytes(
29        &self,
30        req: Request<Bytes>,
31    ) -> WasmBoxedFuture<'static, Result<Response<LazyBody<Bytes>>>>;
32
33    fn send_multipart_bytes(
34        &self,
35        req: Request<MultipartForm>,
36    ) -> WasmBoxedFuture<'static, Result<Response<LazyBody<Bytes>>>>;
37
38    /// Borrows `self`: [`HttpClientExt::send_streaming`] does not promise a
39    /// `'static` future, so neither can its erasure.
40    fn send_streaming_bytes(
41        &self,
42        req: Request<Bytes>,
43    ) -> WasmBoxedFuture<'_, Result<StreamingResponse>>;
44}
45
46impl<H> ErasedHttpClient for H
47where
48    H: HttpClientExt + 'static,
49{
50    fn send_bytes(
51        &self,
52        req: Request<Bytes>,
53    ) -> WasmBoxedFuture<'static, Result<Response<LazyBody<Bytes>>>> {
54        Box::pin(self.send::<Bytes, Bytes>(req))
55    }
56
57    fn send_multipart_bytes(
58        &self,
59        req: Request<MultipartForm>,
60    ) -> WasmBoxedFuture<'static, Result<Response<LazyBody<Bytes>>>> {
61        Box::pin(self.send_multipart::<Bytes>(req))
62    }
63
64    fn send_streaming_bytes(
65        &self,
66        req: Request<Bytes>,
67    ) -> WasmBoxedFuture<'_, Result<StreamingResponse>> {
68        Box::pin(self.send_streaming::<Bytes>(req))
69    }
70}
71
72/// A type-erased, cheaply cloneable HTTP transport.
73///
74/// Clones share the transport and middleware instances but copy the middleware
75/// list. Erasing an already erased transport clones it without another layer.
76/// `Debug` prints only the type name to avoid exposing transport credentials.
77/// The transport is not serializable.
78///
79/// ```compile_fail
80/// fn assert_serialize<T: serde::Serialize>() {}
81/// assert_serialize::<rig_http::http_client::DynHttpClient>();
82/// ```
83#[derive(Clone)]
84pub struct DynHttpClient {
85    inner: Arc<dyn ErasedHttpClient>,
86    /// Transport-boundary middleware, applied in attachment order around
87    /// every request this handle sends. Cloned handles share the same stack.
88    middleware: Vec<Arc<dyn HttpMiddleware>>,
89}
90
91impl DynHttpClient {
92    /// Erase `http`. If `http` is already a `DynHttpClient`, this is a clone
93    /// (its attached middleware included).
94    pub fn new<H>(http: H) -> Self
95    where
96        H: HttpClientExt + 'static,
97    {
98        if let Some(already) = (&http as &dyn Any).downcast_ref::<DynHttpClient>() {
99            return already.clone();
100        }
101        Self {
102            inner: Arc::new(http),
103            middleware: Vec::new(),
104        }
105    }
106
107    /// Attach a transport-boundary [`HttpMiddleware`] to this handle.
108    ///
109    /// Hooks run in attachment order with the phases defined by [`HttpMiddleware`].
110    /// Existing clones keep their previous stack. The underlying transport stays
111    /// shared, so [`Self::ptr_eq`] is unaffected.
112    pub fn with_middleware<M>(mut self, middleware: M) -> Self
113    where
114        M: HttpMiddleware + 'static,
115    {
116        self.middleware.push(Arc::new(middleware));
117        self
118    }
119
120    /// Whether two handles share the same underlying transport.
121    ///
122    /// Compares the transport only: handles that differ in attached
123    /// middleware but wrap the same transport still compare equal.
124    pub fn ptr_eq(&self, other: &Self) -> bool {
125        Arc::ptr_eq(&self.inner, &other.inner)
126    }
127
128    /// Run the request-side middleware phases: all header hooks in order,
129    /// then all body hooks in order (each seeing the final headers).
130    async fn apply_request_middleware(
131        &self,
132        parts: &mut http::request::Parts,
133        body: Bytes,
134    ) -> Result<Bytes> {
135        for mw in &self.middleware {
136            mw.before_request_headers(&parts.method, &parts.uri, &mut parts.headers)
137                .await?;
138        }
139        let mut body = body;
140        for mw in &self.middleware {
141            body = mw
142                .before_request_body(&parts.method, &parts.uri, &parts.headers, body)
143                .await?;
144        }
145        Ok(body)
146    }
147
148    /// Run only the header hooks (the multipart path, which has no single
149    /// serialized body to hand to the body hooks).
150    async fn apply_header_middleware(&self, parts: &mut http::request::Parts) -> Result<()> {
151        for mw in &self.middleware {
152            mw.before_request_headers(&parts.method, &parts.uri, &mut parts.headers)
153                .await?;
154        }
155        Ok(())
156    }
157
158    /// Run the response hooks in attachment order.
159    async fn apply_response_middleware(
160        &self,
161        method: &http::Method,
162        uri: &http::Uri,
163        status: http::StatusCode,
164        headers: &http::HeaderMap,
165    ) -> Result<()> {
166        for mw in &self.middleware {
167            mw.after_response(method, uri, status, headers).await?;
168        }
169        Ok(())
170    }
171}
172
173impl fmt::Debug for DynHttpClient {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        f.write_str("DynHttpClient")
176    }
177}
178
179impl HttpClientExt for DynHttpClient {
180    fn send<T, U>(
181        &self,
182        req: Request<T>,
183    ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
184    where
185        T: Into<Bytes>,
186        T: WasmCompatSend,
187        U: From<Bytes>,
188        U: WasmCompatSend + 'static,
189    {
190        let this = self.clone();
191        let (mut parts, body) = req.map(Into::into).into_parts();
192        async move {
193            let body = this.apply_request_middleware(&mut parts, body).await?;
194            let (method, uri) = (parts.method.clone(), parts.uri.clone());
195            let response = this
196                .inner
197                .send_bytes(Request::from_parts(parts, body))
198                .await?;
199            this.apply_response_middleware(&method, &uri, response.status(), response.headers())
200                .await?;
201            Ok(convert_body::<U>(response))
202        }
203    }
204
205    fn send_multipart<U>(
206        &self,
207        req: Request<MultipartForm>,
208    ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
209    where
210        U: From<Bytes>,
211        U: WasmCompatSend + 'static,
212    {
213        let this = self.clone();
214        let (mut parts, body) = req.into_parts();
215        async move {
216            this.apply_header_middleware(&mut parts).await?;
217            let (method, uri) = (parts.method.clone(), parts.uri.clone());
218            let response = this
219                .inner
220                .send_multipart_bytes(Request::from_parts(parts, body))
221                .await?;
222            this.apply_response_middleware(&method, &uri, response.status(), response.headers())
223                .await?;
224            Ok(convert_body::<U>(response))
225        }
226    }
227
228    fn send_streaming<T>(
229        &self,
230        req: Request<T>,
231    ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
232    where
233        T: Into<Bytes> + WasmCompatSend,
234    {
235        let (mut parts, body) = req.map(Into::into).into_parts();
236        async move {
237            let body = self.apply_request_middleware(&mut parts, body).await?;
238            let (method, uri) = (parts.method.clone(), parts.uri.clone());
239            let response = self
240                .inner
241                .send_streaming_bytes(Request::from_parts(parts, body))
242                .await?;
243            self.apply_response_middleware(&method, &uri, response.status(), response.headers())
244                .await?;
245            Ok(response)
246        }
247    }
248}
249
250fn convert_body<U>(response: Response<LazyBody<Bytes>>) -> Response<LazyBody<U>>
251where
252    U: From<Bytes> + WasmCompatSend + 'static,
253{
254    response.map(|body| -> LazyBody<U> { Box::pin(async move { body.await.map(U::from) }) })
255}
256
257#[cfg(test)]
258mod tests;