Cookbook / Middleware and state

Middleware

Closure middleware, middleware as a type, scoping to a router or one route, and passing data to handlers.

Middleware wraps requests in the order it is added. Server middleware covers every request, including 404s and static files; router and route middleware cover only their own routes. Use from_fn for a quick closure, or implement Middleware for something reusable.

src/main.rs
use axeon::middleware::{from_fn, Middleware, MiddlewareResult, Next};
use axeon::{json, Json, Request, Router, Server, ServerError};
use std::time::Instant;

/// The caller identified by the API key check.
#[derive(Clone)]
struct Caller(String);

/// Rejects requests without the right `X-Api-Key`.
struct ApiKey {
    key: String,
}

impl Middleware for ApiKey {
    fn call(&self, mut req: Request, next: Next) -> MiddlewareResult {
        if req.get_header("x-api-key") != Some(self.key.as_str()) {
            return Box::pin(async { Err(ServerError::Unauthorized("missing or wrong API key".into())) });
        }
        req.insert_extension(Caller("demo-user".into()));
        next.handle(req)
    }
}

fn main() {
    let mut app = Server::new();

    // Times every request, including 404s.
    app.middleware(from_fn(|req, next| async move {
        let start = Instant::now();
        let (method, path) = (req.method(), req.path().to_string());
        let res = next.handle(req).await?;
        println!("[{}] {} {} - {:?}", res.status, method, path, start.elapsed());
        Ok(res)
    }));

    app.get("/public", |_req| async { "open to everyone" });

    // Everything under /api needs the key.
    let mut api = Router::new();
    api.middleware(ApiKey { key: "secret".into() });
    api.get("/profile", |req| async move {
        let caller = req.extension::<Caller>().map(|c| c.0.clone());
        Json(json!({ "user": caller }))
    });
    app.mount("/api", api);

    // Middleware for a single route.
    let admins_only = from_fn(|req, next| async move {
        if req.get_header("x-admin").is_none() {
            return Err(ServerError::Forbidden("admins only".into()));
        }
        next.handle(req).await
    });
    app.route("/posts").get(|_req| async { "all posts" });
    app.route("/posts").middleware(admins_only).post(|_req| async { (201, "created") });

    app.listen("127.0.0.1:3000").expect("server failed to start");
}
Terminal
curl http://127.0.0.1:3000/public
curl -i http://127.0.0.1:3000/api/profile                       # 401
curl http://127.0.0.1:3000/api/profile -H 'x-api-key: secret'   # {"user":"demo-user"}

Notes#

  • req.insert_extension(value) attaches typed data for later middleware and handlers; read it back with req.extension::<T>().
  • Because errors reach middleware as responses, next.handle(req).await? is safe: headers you add afterwards still appear on error responses.
  • Prefer a ready-made one when it exists: see Hardening an API.