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}