Skip to main content

rainier_middleware/
pipeline.rs

1//! The request pipeline — [`Middleware`], [`Next`] and [`Pipeline`].
2//!
3//! The middleware signature is `handle(request, next)`, and that shape is the
4//! whole point: middleware sits *around* the rest of the chain rather than
5//! merely before it, so one piece of middleware can inspect the response,
6//! wrap the call in a transaction or a timer, or decline to call `next` at
7//! all.
8//!
9//! ```
10//! use rainier_http::{Request, Response, StatusCode};
11//! use rainier_middleware::{Middleware, Next};
12//!
13//! struct BlockRobots;
14//!
15//! #[async_trait::async_trait]
16//! impl Middleware for BlockRobots {
17//!     async fn handle(&self, request: Request, next: Next) -> Response {
18//!         if request.header("user-agent").is_some_and(|ua| ua.contains("bot")) {
19//!             return Response::new(StatusCode::FORBIDDEN);   // short-circuit
20//!         }
21//!         let response = next.run(request).await;            // …or continue
22//!         response.with_header("x-checked", "yes")           // …and adjust
23//!     }
24//! }
25//! ```
26
27use std::any::TypeId;
28use std::sync::Arc;
29
30use rainier_http::{Request, Response};
31use rainier_support::BoxedFuture;
32
33/// The concrete type behind a `dyn Middleware`.
34///
35/// A supertrait with a blanket impl rather than a defaulted method, because a
36/// provided method returning `TypeId::of::<Self>()` needs `Self: Sized` and
37/// would therefore not land in the vtable — where a `dyn Middleware` is the
38/// only place it is ever wanted.
39///
40/// This is what makes `Route::without_middleware::<T>()` possible without
41/// going back to comparing names.
42///
43/// The blanket impl is bound on [`Middleware`] rather than on
44/// `Send + Sync + 'static`, and that is load-bearing. A wider bound would also
45/// cover `Arc<dyn Middleware>`, and then `arc.concrete_type_id()` would resolve
46/// to the `Arc`'s own type before auto-deref reached the trait object —
47/// answering `TypeId::of::<Arc<dyn Middleware>>()` for every stage, so every
48/// comparison matches everything. Narrowing it means that call cannot compile
49/// against the wrapper and must go through the vtable, which is the only place
50/// the real answer lives.
51pub trait ConcreteMiddleware: Send + Sync + 'static {
52    /// The `TypeId` of the implementing type.
53    fn concrete_type_id(&self) -> TypeId;
54
55    /// Its fully-qualified type name.
56    fn concrete_type_name(&self) -> &'static str;
57}
58
59impl<T: Middleware> ConcreteMiddleware for T {
60    fn concrete_type_id(&self) -> TypeId {
61        TypeId::of::<T>()
62    }
63
64    fn concrete_type_name(&self) -> &'static str {
65        std::any::type_name::<T>()
66    }
67}
68
69/// A stage in the request pipeline.
70#[async_trait::async_trait]
71pub trait Middleware: ConcreteMiddleware {
72    /// Handle the request, optionally calling `next`.
73    ///
74    /// Returning without calling `next.run(..)` short-circuits the pipeline —
75    /// the handler and every later middleware are skipped. That is how
76    /// authentication, rate limiting and CORS preflight all work.
77    async fn handle(&self, request: Request, next: Next) -> Response;
78
79    /// A label for diagnostics and `route:list`.
80    ///
81    /// Defaults to the type's short name. This is a **label**, not an
82    /// identifier: nothing looks middleware up by it, so two stages sharing one
83    /// is untidy rather than broken.
84    fn name(&self) -> &'static str {
85        short_type_name(self.concrete_type_name())
86    }
87}
88
89/// `a::b::Thing<x::y::Z>` → `Thing<Z>`.
90///
91/// `route:list` is a table; a column of fully-qualified paths makes it
92/// unreadable and tells you nothing the short name does not.
93pub(crate) fn short_type_name(full: &'static str) -> &'static str {
94    // Generic parameters are dropped along with the path, which is what keeps
95    // `Authenticate<app::models::User>` from being wider than the terminal.
96    let without_generics = full.split_once('<').map_or(full, |(head, _)| head);
97    without_generics.rsplit("::").next().unwrap_or(without_generics)
98}
99
100/// The end of the pipeline — the thing that actually produces a response.
101///
102/// The router implements this; the pipeline itself has no idea what a route
103/// is, which is what keeps this crate independent of `rainier-routing`.
104pub trait Destination: Send + Sync + 'static {
105    /// Produce the response.
106    fn call(&self, request: Request) -> BoxedFuture<Response>;
107}
108
109/// Any async closure is a destination — useful for tests and for one-off
110/// pipelines.
111impl<F, Fut> Destination for F
112where
113    F: Fn(Request) -> Fut + Send + Sync + 'static,
114    Fut: std::future::Future<Output = Response> + Send + 'static,
115{
116    fn call(&self, request: Request) -> BoxedFuture<Response> {
117        Box::pin(self(request))
118    }
119}
120
121/// The remainder of the pipeline, handed to each middleware.
122pub struct Next {
123    stages: Arc<[Arc<dyn Middleware>]>,
124    index: usize,
125    destination: Arc<dyn Destination>,
126}
127
128impl Next {
129    /// Continue: run the next middleware, or the destination if this was the
130    /// last one.
131    pub fn run(self, request: Request) -> BoxedFuture<Response> {
132        let Some(stage) = self.stages.get(self.index).cloned() else {
133            return self.destination.call(request);
134        };
135
136        let next =
137            Next { stages: self.stages, index: self.index + 1, destination: self.destination };
138
139        // The `Arc` is moved into the future so the borrow of `*stage` that
140        // `handle` takes stays alive for as long as the future does.
141        Box::pin(async move { stage.handle(request, next).await })
142    }
143
144    /// How many stages are still to run, excluding the destination.
145    pub fn remaining(&self) -> usize {
146        self.stages.len().saturating_sub(self.index)
147    }
148}
149
150impl std::fmt::Debug for Next {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("Next").field("remaining", &self.remaining()).finish()
153    }
154}
155
156/// Builds and runs a pipeline: a request, through some middleware, to a
157/// destination.
158///
159/// ```
160/// # use rainier_http::{Request, Response};
161/// # use rainier_middleware::Pipeline;
162/// # #[tokio::main] async fn main() {
163/// let response = Pipeline::new()
164///     .then(|_req: Request| async { Response::text("hello") })
165///     .run(Request::builder().build())
166///     .await;
167///
168/// assert_eq!(response.status(), 200);
169/// # }
170/// ```
171#[derive(Default)]
172pub struct Pipeline {
173    stages: Vec<Arc<dyn Middleware>>,
174}
175
176impl Pipeline {
177    /// An empty pipeline.
178    pub fn new() -> Self {
179        Self::default()
180    }
181
182    /// Append one middleware.
183    pub fn through(mut self, middleware: impl Middleware) -> Self {
184        self.stages.push(Arc::new(middleware));
185        self
186    }
187
188    /// Append an already-shared middleware.
189    pub fn through_arc(mut self, middleware: Arc<dyn Middleware>) -> Self {
190        self.stages.push(middleware);
191        self
192    }
193
194    /// Append several already-shared middleware, in order.
195    pub fn through_all(
196        mut self,
197        middleware: impl IntoIterator<Item = Arc<dyn Middleware>>,
198    ) -> Self {
199        self.stages.extend(middleware);
200        self
201    }
202
203    /// Close the pipeline with its destination.
204    pub fn then(self, destination: impl Destination) -> ReadyPipeline {
205        self.then_arc(Arc::new(destination))
206    }
207
208    /// [`then`](Self::then) with an already-shared destination.
209    pub fn then_arc(self, destination: Arc<dyn Destination>) -> ReadyPipeline {
210        ReadyPipeline { stages: self.stages.into(), destination }
211    }
212
213    /// How many stages are registered.
214    pub fn len(&self) -> usize {
215        self.stages.len()
216    }
217
218    /// Whether the pipeline has no middleware.
219    pub fn is_empty(&self) -> bool {
220        self.stages.is_empty()
221    }
222}
223
224/// A pipeline with its destination attached, ready to run — and cheap to
225/// re-run, which is what lets the router build one per route at boot rather
226/// than per request.
227#[derive(Clone)]
228pub struct ReadyPipeline {
229    stages: Arc<[Arc<dyn Middleware>]>,
230    destination: Arc<dyn Destination>,
231}
232
233impl ReadyPipeline {
234    /// Send a request through.
235    pub async fn run(&self, request: Request) -> Response {
236        Next {
237            stages: Arc::clone(&self.stages),
238            index: 0,
239            destination: Arc::clone(&self.destination),
240        }
241        .run(request)
242        .await
243    }
244
245    /// The names of the middleware, in order.
246    pub fn stage_names(&self) -> Vec<&'static str> {
247        self.stages.iter().map(|s| s.name()).collect()
248    }
249}
250
251impl std::fmt::Debug for ReadyPipeline {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.debug_struct("ReadyPipeline").field("stages", &self.stage_names()).finish()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use rainier_http::StatusCode;
261    use std::sync::Mutex;
262
263    /// Records when it runs, on the way in and on the way out.
264    struct Trace {
265        tag: &'static str,
266        log: Arc<Mutex<Vec<String>>>,
267    }
268
269    #[async_trait::async_trait]
270    impl Middleware for Trace {
271        async fn handle(&self, request: Request, next: Next) -> Response {
272            self.log.lock().unwrap().push(format!("{}:before", self.tag));
273            let response = next.run(request).await;
274            self.log.lock().unwrap().push(format!("{}:after", self.tag));
275            response
276        }
277
278        fn name(&self) -> &'static str {
279            "Trace"
280        }
281    }
282
283    struct ShortCircuit;
284
285    #[async_trait::async_trait]
286    impl Middleware for ShortCircuit {
287        async fn handle(&self, _request: Request, _next: Next) -> Response {
288            Response::new(StatusCode::FORBIDDEN)
289        }
290    }
291
292    fn log() -> Arc<Mutex<Vec<String>>> {
293        Arc::new(Mutex::new(Vec::new()))
294    }
295
296    #[tokio::test]
297    async fn an_empty_pipeline_reaches_the_destination() {
298        let response = Pipeline::new()
299            .then(|_: Request| async { Response::text("hi") })
300            .run(Request::builder().build())
301            .await;
302        assert_eq!(response.status(), StatusCode::OK);
303    }
304
305    #[tokio::test]
306    async fn middleware_wraps_the_destination_in_order() {
307        let log = log();
308        let recorded = Arc::clone(&log);
309
310        let destination_log = Arc::clone(&log);
311        let response = Pipeline::new()
312            .through(Trace { tag: "outer", log: Arc::clone(&log) })
313            .through(Trace { tag: "inner", log: Arc::clone(&log) })
314            .then(move |_: Request| {
315                let log = Arc::clone(&destination_log);
316                async move {
317                    log.lock().unwrap().push("handler".into());
318                    Response::text("ok")
319                }
320            })
321            .run(Request::builder().build())
322            .await;
323
324        assert_eq!(response.status(), StatusCode::OK);
325        assert_eq!(
326            *recorded.lock().unwrap(),
327            vec!["outer:before", "inner:before", "handler", "inner:after", "outer:after"]
328        );
329    }
330
331    #[tokio::test]
332    async fn short_circuiting_skips_the_rest_and_the_handler() {
333        let log = log();
334        let recorded = Arc::clone(&log);
335
336        let response = Pipeline::new()
337            .through(Trace { tag: "outer", log: Arc::clone(&log) })
338            .through(ShortCircuit)
339            .through(Trace { tag: "never", log: Arc::clone(&log) })
340            .then(|_: Request| async { Response::text("unreachable") })
341            .run(Request::builder().build())
342            .await;
343
344        assert_eq!(response.status(), StatusCode::FORBIDDEN);
345        // The outer middleware still sees the response on the way out.
346        assert_eq!(*recorded.lock().unwrap(), vec!["outer:before", "outer:after"]);
347    }
348
349    #[tokio::test]
350    async fn middleware_can_rewrite_the_request_and_the_response() {
351        struct Tag;
352
353        #[async_trait::async_trait]
354        impl Middleware for Tag {
355            async fn handle(&self, mut request: Request, next: Next) -> Response {
356                request.merge_input(serde_json::json!({ "injected": "yes" }));
357                next.run(request).await.with_header("x-tagged", "1")
358            }
359        }
360
361        let response =
362            Pipeline::new()
363                .through(Tag)
364                .then(|request: Request| async move {
365                    Response::text(request.input_or("injected", "no"))
366                })
367                .run(Request::builder().build())
368                .await;
369
370        assert_eq!(response.header("x-tagged"), Some("1"));
371        let body = response.into_http().into_body().collect().await.unwrap();
372        assert_eq!(body, "yes");
373    }
374
375    #[tokio::test]
376    async fn a_ready_pipeline_can_be_run_repeatedly() {
377        let pipeline = Pipeline::new()
378            .through(Trace { tag: "a", log: log() })
379            .then(|_: Request| async { Response::text("ok") });
380
381        for _ in 0..3 {
382            assert_eq!(pipeline.run(Request::builder().build()).await.status(), StatusCode::OK);
383        }
384        assert_eq!(pipeline.stage_names(), vec!["Trace"]);
385    }
386
387    #[tokio::test]
388    async fn next_reports_how_much_is_left() {
389        struct CountRemaining(Arc<Mutex<Vec<usize>>>);
390
391        #[async_trait::async_trait]
392        impl Middleware for CountRemaining {
393            async fn handle(&self, request: Request, next: Next) -> Response {
394                self.0.lock().unwrap().push(next.remaining());
395                next.run(request).await
396            }
397        }
398
399        let seen = Arc::new(Mutex::new(Vec::new()));
400        Pipeline::new()
401            .through(CountRemaining(Arc::clone(&seen)))
402            .through(CountRemaining(Arc::clone(&seen)))
403            .then(|_: Request| async { Response::no_content() })
404            .run(Request::builder().build())
405            .await;
406
407        assert_eq!(*seen.lock().unwrap(), vec![1, 0]);
408    }
409}