AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/http/src/middleware.rs

Latest change 556d41481651d98d322f87107309ae80f6f96580 - feat(http): middleware chain + built-ins by Ólafur Búi Ólafsson

//! A composable middleware chain that wraps a [`Handler`](crate::Handler).
//!
//! A [`Middleware`] sits *around* the request: it can inspect the [`Request`],
//! decide whether to call `next` (the rest of the chain, ending in the handler),
//! and post-process the [`Reply`] on the way back out. Middleware compose into an
//! ordered [`MiddlewareStack`]; the **first pushed runs outermost** — it sees the
//! request first and the reply last, wrapping everything inside it.
//!
//! This is an *additive* capability. The existing [`Server::run`](crate::Server::run)
//! path is unchanged; opt in with [`Server::run_with`](crate::Server::run_with).
//! An empty stack is a no-op that simply calls the handler.

use crate::{Reply, Request};
use std::sync::Arc;

/// One layer of request/response processing.
///
/// `handle` receives the request and `next` — a callable that runs the rest of
/// the chain (the inner middleware, ending in the handler). A middleware may:
///
/// * call `next(req)` and return its [`Reply`] unchanged (pass-through),
/// * call `next(req)` and modify the returned [`Reply`] (post-process), or
/// * return a [`Reply`] *without* calling `next` (short-circuit, e.g. a block).
///
/// `Send + Sync + 'static` so a stack can be shared across worker threads. A
/// closure of the right shape is a `Middleware` for free via the blanket impl.
pub trait Middleware: Send + Sync + 'static {
    fn handle(&self, req: &Request, next: &dyn Fn(&Request) -> Reply) -> Reply;
}

impl<F> Middleware for F
where
    F: Fn(&Request, &dyn Fn(&Request) -> Reply) -> Reply + Send + Sync + 'static,
{
    fn handle(&self, req: &Request, next: &dyn Fn(&Request) -> Reply) -> Reply {
        self(req, next)
    }
}

/// An ordered stack of [`Middleware`] that runs before a handler.
///
/// Build with [`MiddlewareStack::new`] then [`push`](MiddlewareStack::push) in
/// outermost-first order. The stack is cheap to clone-share (`Arc` layers) and
/// is consumed by [`Server::run_with`](crate::Server::run_with).
#[derive(Clone, Default)]
pub struct MiddlewareStack {
    layers: Vec<Arc<dyn Middleware>>,
}

impl MiddlewareStack {
    /// An empty stack — running it is identical to calling the handler directly.
    pub fn new() -> MiddlewareStack {
        MiddlewareStack { layers: Vec::new() }
    }

    /// Append a middleware. The first pushed runs outermost (sees the request
    /// first, the reply last). Chainable.
    pub fn push<M: Middleware>(mut self, mw: M) -> MiddlewareStack {
        self.layers.push(Arc::new(mw));
        self
    }

    /// Number of layers.
    pub fn len(&self) -> usize {
        self.layers.len()
    }

    /// Whether the stack has no layers (a no-op).
    pub fn is_empty(&self) -> bool {
        self.layers.is_empty()
    }

    /// Run the request through every layer and then `handler`, returning the
    /// final (possibly post-processed) [`Reply`]. The chain is built from the
    /// inside out so the first-pushed layer ends up outermost.
    pub fn handle<'a>(&'a self, req: &Request, handler: &'a dyn Fn(&Request) -> Reply) -> Reply {
        let mut next: Box<dyn Fn(&Request) -> Reply + 'a> = Box::new(handler);
        for mw in self.layers.iter().rev() {
            let prev = next;
            next = Box::new(move |r: &Request| mw.handle(r, &prev));
        }
        next(req)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Response;
    use std::sync::Mutex;

    fn get(path: &str) -> Request {
        Request::parse_head(format!("GET {path} HTTP/1.1\r\n\r\n").as_bytes()).unwrap()
    }

    fn response(reply: Reply) -> Response {
        match reply {
            Reply::Response(r) => r,
            Reply::Upgrade(_) => panic!("expected a Response, got an Upgrade"),
        }
    }

    fn header<'a>(r: &'a Response, name: &str) -> Option<&'a str> {
        r.headers
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(name))
            .map(|(_, v)| v.as_str())
    }

    #[test]
    fn empty_stack_just_calls_the_handler() {
        let stack = MiddlewareStack::new();
        assert!(stack.is_empty());
        let reply = stack.handle(&get("/"), &|_| {
            Reply::Response(Response::ok().with_text("hi"))
        });
        assert_eq!(response(reply).body, b"hi");
    }

    #[test]
    fn a_middleware_can_add_a_header() {
        let add = |req: &Request, next: &dyn Fn(&Request) -> Reply| match next(req) {
            Reply::Response(r) => Reply::Response(r.with_header("X-Added", "yes")),
            other => other,
        };
        let stack = MiddlewareStack::new().push(add);
        let reply = stack.handle(&get("/"), &|_| Reply::Response(Response::ok()));
        assert_eq!(header(&response(reply), "X-Added"), Some("yes"));
    }

    #[test]
    fn a_middleware_can_short_circuit_without_calling_next() {
        let reached = Arc::new(Mutex::new(false));
        let flag = Arc::clone(&reached);

        let block = |req: &Request, next: &dyn Fn(&Request) -> Reply| {
            if req.path == "/blocked" {
                return Reply::Response(Response::new(403).with_text("blocked"));
            }
            next(req)
        };
        let stack = MiddlewareStack::new().push(block);

        let handler = move |_req: &Request| {
            *flag.lock().unwrap() = true;
            Reply::Response(Response::ok())
        };
        let reply = stack.handle(&get("/blocked"), &handler);

        assert_eq!(response(reply).status, 403);
        assert!(
            !*reached.lock().unwrap(),
            "handler must not run when blocked"
        );
    }

    #[test]
    fn outer_middleware_wraps_inner() {
        let log = Arc::new(Mutex::new(Vec::<&'static str>::new()));

        let l_outer = Arc::clone(&log);
        let outer = move |req: &Request, next: &dyn Fn(&Request) -> Reply| {
            l_outer.lock().unwrap().push("outer-before");
            let reply = next(req);
            l_outer.lock().unwrap().push("outer-after");
            reply
        };

        let l_inner = Arc::clone(&log);
        let inner = move |req: &Request, next: &dyn Fn(&Request) -> Reply| {
            l_inner.lock().unwrap().push("inner-before");
            let reply = next(req);
            l_inner.lock().unwrap().push("inner-after");
            reply
        };

        let stack = MiddlewareStack::new().push(outer).push(inner);
        assert_eq!(stack.len(), 2);
        let _ = stack.handle(&get("/"), &|_| Reply::Response(Response::ok()));

        assert_eq!(
            *log.lock().unwrap(),
            vec!["outer-before", "inner-before", "inner-after", "outer-after"]
        );
    }
}