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::{Exchange, 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        self.apply_header_middleware(parts).await?;
136        let exchange = Exchange {
137            method: &parts.method,
138            uri: &parts.uri,
139            extensions: &parts.extensions,
140        };
141        let mut body = body;
142        for mw in &self.middleware {
143            body = mw
144                .before_request_body(exchange, &parts.headers, body)
145                .await?;
146        }
147        Ok(body)
148    }
149
150    /// Run only the header hooks (the multipart path, which has no single
151    /// serialized body to hand to the body hooks).
152    async fn apply_header_middleware(&self, parts: &mut http::request::Parts) -> Result<()> {
153        for mw in &self.middleware {
154            let exchange = Exchange {
155                method: &parts.method,
156                uri: &parts.uri,
157                extensions: &parts.extensions,
158            };
159            mw.before_request_headers(exchange, &mut parts.headers)
160                .await?;
161        }
162        Ok(())
163    }
164
165    /// Send `parts` and `body` through `send`, then run the response hooks
166    /// in attachment order on its reply: a response, or an error that kept
167    /// a non-success reply's status and headers. A hook's error fails a
168    /// response with that error; an error reply stops at a failing hook and
169    /// still fails with its own error.
170    async fn observe<T, B, F>(
171        &self,
172        parts: http::request::Parts,
173        body: T,
174        send: impl FnOnce(Request<T>) -> F,
175    ) -> Result<Response<B>>
176    where
177        F: Future<Output = Result<Response<B>>>,
178    {
179        let (method, uri, extensions) = (
180            parts.method.clone(),
181            parts.uri.clone(),
182            parts.extensions.clone(),
183        );
184        let exchange = Exchange {
185            method: &method,
186            uri: &uri,
187            extensions: &extensions,
188        };
189        let error = match send(Request::from_parts(parts, body)).await {
190            Ok(response) => {
191                for mw in &self.middleware {
192                    mw.after_response(exchange, response.status(), response.headers())
193                        .await?;
194                }
195                return Ok(response);
196            }
197            Err(error) => error,
198        };
199        if let (Some(status), Some(headers)) =
200            (error.non_success_status(), error.non_success_headers())
201        {
202            for mw in &self.middleware {
203                if mw.after_response(exchange, status, headers).await.is_err() {
204                    break;
205                }
206            }
207        }
208        Err(error)
209    }
210}
211
212impl fmt::Debug for DynHttpClient {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        f.write_str("DynHttpClient")
215    }
216}
217
218impl HttpClientExt for DynHttpClient {
219    fn send<T, U>(
220        &self,
221        req: Request<T>,
222    ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
223    where
224        T: Into<Bytes>,
225        T: WasmCompatSend,
226        U: From<Bytes>,
227        U: WasmCompatSend + 'static,
228    {
229        let this = self.clone();
230        let (mut parts, body) = req.map(Into::into).into_parts();
231        async move {
232            let body = this.apply_request_middleware(&mut parts, body).await?;
233            let response = this
234                .observe(parts, body, |request| this.inner.send_bytes(request))
235                .await?;
236            Ok(convert_body::<U>(response))
237        }
238    }
239
240    fn send_multipart<U>(
241        &self,
242        req: Request<MultipartForm>,
243    ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
244    where
245        U: From<Bytes>,
246        U: WasmCompatSend + 'static,
247    {
248        let this = self.clone();
249        let (mut parts, body) = req.into_parts();
250        async move {
251            this.apply_header_middleware(&mut parts).await?;
252            let response = this
253                .observe(parts, body, |request| {
254                    this.inner.send_multipart_bytes(request)
255                })
256                .await?;
257            Ok(convert_body::<U>(response))
258        }
259    }
260
261    fn send_streaming<T>(
262        &self,
263        req: Request<T>,
264    ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
265    where
266        T: Into<Bytes> + WasmCompatSend,
267    {
268        let (mut parts, body) = req.map(Into::into).into_parts();
269        async move {
270            let body = self.apply_request_middleware(&mut parts, body).await?;
271            self.observe(parts, body, |request| {
272                self.inner.send_streaming_bytes(request)
273            })
274            .await
275        }
276    }
277}
278
279fn convert_body<U>(response: Response<LazyBody<Bytes>>) -> Response<LazyBody<U>>
280where
281    U: From<Bytes> + WasmCompatSend + 'static,
282{
283    response.map(|body| -> LazyBody<U> { Box::pin(async move { body.await.map(U::from) }) })
284}
285
286#[cfg(test)]
287mod tests;