Skip to main content

fetch_kit/middleware/
chain.rs

1//! The core middleware chain: the [`Middleware`] trait, the [`Next`]
2//! continuation, and the [`ClientWithMiddleware`] runner.
3//!
4//! The shape intentionally mirrors `reqwest-middleware` 0.5 so that
5//! migrating an existing middleware is mechanical: swap the import path
6//! from `reqwest_middleware::{Middleware, Next, ...}` to
7//! `fetch_kit::middleware::{Middleware, Next, ...}` and the signature is
8//! unchanged.
9//!
10//! # Ordering semantics
11//!
12//! Middleware form an onion around the transport. The middleware
13//! registered **first** is the **outermost** layer: it observes the
14//! request first and the response last.
15//!
16//! ```text
17//! ClientBuilder::new(client)
18//!     .with(AuthMiddleware)      // 1st registered → outermost
19//!     .with(RetryMiddleware)     // 2nd registered
20//!     .build();
21//!
22//! request  →  Auth  →  Retry  →  transport
23//! response ←  Auth  ←  Retry  ←  ╹
24//! ```
25
26use std::future::Future;
27use std::sync::Arc;
28
29use async_trait::async_trait;
30use http::Extensions;
31use reqwest::{Method, Request, Response};
32
33/// Result alias used throughout the middleware stack.
34pub type Result<T, E = Error> = std::result::Result<T, E>;
35
36/// Error type produced by the middleware stack.
37///
38/// Mirrors `reqwest-middleware`'s error: either the underlying
39/// `reqwest::Error`, or an opaque error raised by a middleware
40/// (e.g. [`crate::FetchError::CircuitOpen`]).
41#[derive(Debug, thiserror::Error)]
42pub enum Error {
43    /// A middleware produced an error.
44    #[error("middleware failed: {0}")]
45    Middleware(#[from] Box<dyn std::error::Error + Send + Sync>),
46
47    /// The underlying `reqwest` client produced an error.
48    #[error(transparent)]
49    Reqwest(#[from] reqwest::Error),
50}
51
52/// Async middleware that wraps a [`ClientWithMiddleware`].
53///
54/// Implement `handle` to inspect or rewrite the request before it travels
55/// inward, and/or the response (or error) on the way back out. Call
56/// [`Next::run`] to continue the chain; returning without calling it
57/// short-circuits everything inward (including the transport).
58///
59/// The `extensions` argument is a per-logical-request typed map shared by
60/// every middleware in the chain (and across all retry attempts of one
61/// logical request); use it for cross-middleware state. Request-scoped
62/// typed data set via [`RequestBuilder::with_extension`] arrives in this
63/// map.
64#[async_trait]
65pub trait Middleware: 'static + Send + Sync {
66    /// Handle the request, optionally calling `next` to continue the chain.
67    async fn handle(
68        &self,
69        req: Request,
70        extensions: &mut Extensions,
71        next: Next<'_>,
72    ) -> Result<Response>;
73}
74
75/// The continuation passed to [`Middleware::handle`].
76///
77/// `run` executes the remainder of the chain (the middleware registered
78/// after this one, then the transport). `Next` is cheap to clone, which is
79/// how retry-style middleware issue multiple attempts.
80#[derive(Clone)]
81pub struct Next<'a> {
82    client: &'a ClientWithMiddleware,
83    remaining: &'a [Arc<dyn Middleware>],
84}
85
86impl Next<'_> {
87    pub(crate) fn new<'a>(
88        client: &'a ClientWithMiddleware,
89        remaining: &'a [Arc<dyn Middleware>],
90    ) -> Next<'a> {
91        Next { client, remaining }
92    }
93
94    /// Continue the chain with the (possibly rewritten) request.
95    pub fn run(
96        self,
97        req: Request,
98        extensions: &mut Extensions,
99    ) -> impl Future<Output = Result<Response>> {
100        let client = self.client;
101        let remaining = self.remaining;
102        async move { execute_with_chain(client, remaining, req, extensions).await }
103    }
104}
105
106/// Execute `req` through `middleware` and finally `client`.
107pub(crate) async fn execute_with_chain(
108    client: &ClientWithMiddleware,
109    middleware: &[Arc<dyn Middleware>],
110    req: Request,
111    extensions: &mut Extensions,
112) -> Result<Response> {
113    if let Some((current, rest)) = middleware.split_first() {
114        current
115            .handle(req, extensions, Next::new(client, rest))
116            .await
117    } else {
118        client.client.execute(req).await.map_err(Error::Reqwest)
119    }
120}
121
122/// A `reqwest::Client` wrapped in a middleware chain.
123///
124/// Cloneable (all clones share the same chain and connection pool) and
125/// `Debug`. Built with [`ClientBuilder`].
126#[derive(Clone)]
127pub struct ClientWithMiddleware {
128    client: reqwest::Client,
129    middleware: Arc<[Arc<dyn Middleware>]>,
130}
131
132impl ClientWithMiddleware {
133    pub(crate) fn new(client: reqwest::Client, middleware: Arc<[Arc<dyn Middleware>]>) -> Self {
134        Self { client, middleware }
135    }
136
137    /// The number of middleware registered on this client.
138    pub fn middleware_count(&self) -> usize {
139        self.middleware.len()
140    }
141
142    /// Return a new client with `middleware` appended **inside** the
143    /// existing chain (closest to the transport).
144    ///
145    /// This is a composition point for clients that were already built
146    /// (e.g. [`crate::Client::inner`]). When building from scratch, prefer
147    /// [`ClientBuilder::with`], which registers middleware outermost-first.
148    pub fn with_middleware(mut self, middleware: Arc<dyn Middleware>) -> Self {
149        let mut mws = Vec::with_capacity(self.middleware.len() + 1);
150        mws.extend(self.middleware.iter().cloned());
151        mws.push(middleware);
152        self.middleware = mws.into();
153        self
154    }
155
156    /// Execute the request through the middleware chain with a fresh,
157    /// empty per-request extension map.
158    pub async fn execute(&self, req: Request) -> Result<Response> {
159        let mut extensions = Extensions::new();
160        self.execute_with_extensions(req, &mut extensions).await
161    }
162
163    /// Execute the request through the middleware chain, seeding the
164    /// per-request extension map with `extensions`.
165    ///
166    /// The map is shared by every middleware in the chain and survives
167    /// across retry attempts of the same logical request; it is the
168    /// request-scoped typed map that [`RequestBuilder::with_extension`]
169    /// populates.
170    pub async fn execute_with_extensions(
171        &self,
172        req: Request,
173        extensions: &mut Extensions,
174    ) -> Result<Response> {
175        execute_with_chain(self, &self.middleware, req, extensions).await
176    }
177
178    /// Convenience method to make a `GET` request to a URL.
179    pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
180        self.request(Method::GET, url)
181    }
182
183    /// Convenience method to make a `POST` request to a URL.
184    pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
185        self.request(Method::POST, url)
186    }
187
188    /// Convenience method to make a `PUT` request to a URL.
189    pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
190        self.request(Method::PUT, url)
191    }
192
193    /// Convenience method to make a `PATCH` request to a URL.
194    pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
195        self.request(Method::PATCH, url)
196    }
197
198    /// Convenience method to make a `HEAD` request to a URL.
199    pub fn head(&self, url: impl Into<String>) -> RequestBuilder {
200        self.request(Method::HEAD, url)
201    }
202
203    /// Convenience method to make a `DELETE` request to a URL.
204    pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
205        self.request(Method::DELETE, url)
206    }
207
208    /// Start building a request with the given method and URL.
209    pub fn request(&self, method: Method, url: impl Into<String>) -> RequestBuilder {
210        RequestBuilder {
211            client: self.clone(),
212            inner: self.client.request(method, url.into()),
213            extensions: Vec::new(),
214        }
215    }
216}
217
218impl std::fmt::Debug for ClientWithMiddleware {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        f.debug_struct("ClientWithMiddleware")
221            .field("client", &self.client)
222            .field("middleware", &self.middleware.len())
223            .finish()
224    }
225}
226
227/// Builder for [`ClientWithMiddleware`]: wrap a `reqwest::Client` with
228/// middleware, **first-registered = outermost**.
229///
230/// Mirrors `reqwest-middleware`'s `ClientBuilder` so migration is
231/// mechanical.
232#[derive(Clone)]
233pub struct ClientBuilder {
234    client: reqwest::Client,
235    middleware: Vec<Arc<dyn Middleware>>,
236}
237
238impl ClientBuilder {
239    /// Wrap the given `reqwest::Client` with a middleware chain.
240    pub fn new(client: reqwest::Client) -> Self {
241        Self {
242            client,
243            middleware: Vec::new(),
244        }
245    }
246
247    /// Register a middleware. The first middleware registered runs
248    /// outermost (sees the request first, the response last).
249    pub fn with<M: Middleware>(self, middleware: M) -> Self {
250        self.with_middleware(Arc::new(middleware))
251    }
252
253    /// Register an already-erased middleware (same ordering as
254    /// [`ClientBuilder::with`]).
255    pub fn with_middleware(mut self, middleware: Arc<dyn Middleware>) -> Self {
256        self.middleware.push(middleware);
257        self
258    }
259
260    /// Build the [`ClientWithMiddleware`].
261    pub fn build(self) -> ClientWithMiddleware {
262        ClientWithMiddleware::new(self.client, self.middleware.into())
263    }
264}
265
266/// One registered "insert this typed value into the request-scoped
267/// extensions map" operation, applied when the request is sent.
268type ExtensionApplier = Arc<dyn Fn(&mut Extensions) + Send + Sync>;
269
270/// A request builder bound to a [`ClientWithMiddleware`].
271///
272/// Wraps `reqwest::RequestBuilder` and adds
273/// [`RequestBuilder::with_extension`] for request-scoped typed data that
274/// every middleware can read (`extensions.get::<T>()` in
275/// [`Middleware::handle`]).
276///
277/// Not `Clone` (reqwest's builder isn't either); use
278/// [`RequestBuilder::try_clone`] for bufferable requests.
279pub struct RequestBuilder {
280    client: ClientWithMiddleware,
281    inner: reqwest::RequestBuilder,
282    extensions: Vec<ExtensionApplier>,
283}
284
285impl RequestBuilder {
286    /// Attach request-scoped typed data, visible to every middleware in
287    /// the chain through the `extensions` argument of
288    /// [`Middleware::handle`] (`extensions.get::<T>()`). The map is shared
289    /// across retry attempts of the same logical request, so the data
290    /// survives retries.
291    pub fn with_extension<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
292        self.extensions.push(Arc::new(move |ext: &mut Extensions| {
293            ext.insert(value.clone());
294        }));
295        self
296    }
297
298    /// Add a single header.
299    pub fn header<K, V>(mut self, key: K, value: V) -> Self
300    where
301        http::header::HeaderName: TryFrom<K>,
302        <http::header::HeaderName as TryFrom<K>>::Error: Into<http::Error>,
303        http::HeaderValue: TryFrom<V>,
304        <http::HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
305    {
306        self.inner = self.inner.header(key, value);
307        self
308    }
309
310    /// Set all headers from a `HeaderMap`.
311    pub fn headers(mut self, headers: reqwest::header::HeaderMap) -> Self {
312        self.inner = self.inner.headers(headers);
313        self
314    }
315
316    /// Append query parameters.
317    pub fn query<T: serde::Serialize + ?Sized>(mut self, params: &T) -> Self {
318        self.inner = self.inner.query(params);
319        self
320    }
321
322    /// Override the request timeout.
323    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
324        self.inner = self.inner.timeout(timeout);
325        self
326    }
327
328    /// Set a JSON body (requires the `json` feature).
329    #[cfg(feature = "json")]
330    pub fn json<T: serde::Serialize + ?Sized>(mut self, body: &T) -> Self {
331        self.inner = self.inner.json(body);
332        self
333    }
334
335    /// Set a `Bearer` authorization token.
336    pub fn bearer_auth(mut self, token: impl std::fmt::Display) -> Self {
337        self.inner = self.inner.bearer_auth(token);
338        self
339    }
340
341    /// Set HTTP basic auth credentials.
342    pub fn basic_auth<U, P>(mut self, username: U, password: Option<P>) -> Self
343    where
344        U: std::fmt::Display,
345        P: std::fmt::Display,
346    {
347        self.inner = self.inner.basic_auth(username, password);
348        self
349    }
350
351    /// Set the request body directly.
352    pub fn body(mut self, body: impl Into<reqwest::Body>) -> Self {
353        self.inner = self.inner.body(body);
354        self
355    }
356
357    /// Set form-encoded body from a serializable value.
358    pub fn form<T: serde::Serialize + ?Sized>(mut self, form: &T) -> Self {
359        self.inner = self.inner.form(form);
360        self
361    }
362
363    /// Set a multipart form body (requires the `multipart` feature).
364    #[cfg(feature = "multipart")]
365    pub fn multipart(mut self, form: reqwest::multipart::Form) -> Self {
366        self.inner = self.inner.multipart(form);
367        self
368    }
369
370    /// Build the request without sending it.
371    ///
372    /// Extension values registered via [`RequestBuilder::with_extension`]
373    /// are applied by [`RequestBuilder::send`] (they seed the per-request
374    /// extension map passed to middleware, which reqwest's `Request` type
375    /// does not expose).
376    pub fn build(self) -> Result<Request> {
377        Ok(self.inner.build()?)
378    }
379
380    /// Clone the builder, if the underlying request is cloneable
381    /// (buffered bodies are; streaming bodies are not).
382    pub fn try_clone(&self) -> Option<Self> {
383        Some(Self {
384            client: self.client.clone(),
385            inner: self.inner.try_clone()?,
386            extensions: self.extensions.clone(),
387        })
388    }
389
390    /// Build and execute the request through the middleware chain,
391    /// seeding the per-request extension map with any
392    /// [`RequestBuilder::with_extension`] values.
393    pub async fn send(self) -> Result<Response> {
394        let Self {
395            client,
396            inner,
397            extensions: appliers,
398        } = self;
399        let request = inner.build()?;
400        let mut extensions = Extensions::new();
401        for apply in &appliers {
402            apply(&mut extensions);
403        }
404        client
405            .execute_with_extensions(request, &mut extensions)
406            .await
407    }
408}