Middleware
A typed Middleware trait with before_request / after_response hooks, composed in a predictable onion order.
Middleware
axum and tower already give you Layer + Service, but writing one means learning poll-readiness, boxed futures, and the Service ownership rules. Most application middleware only wants two things: look at the request before the handler, and look at the response after. umbral's Middleware trait is exactly that: a before_request / after_response pair, typed for Rust.
The trait
use umbral::prelude::*; // brings in Middlewareuse axum::extract::Request;use axum::response::{IntoResponse, Response};use http::StatusCode; struct RequireApiKey; #[umbral::async_trait]impl Middleware for RequireApiKey { async fn before_request(&self, req: Request) -> Result<Request, Response> { if req.headers().get("x-api-key").is_some() { Ok(req) // continue to the handler } else { Err((StatusCode::UNAUTHORIZED, "missing API key").into_response()) // short-circuit } } async fn after_response(&self, mut res: Response) -> Response { res.headers_mut().insert("x-powered-by", "umbral".parse().unwrap()); res }}Both hooks have a default pass-through, so implement only the one you need. #[umbral::async_trait] is re-exported from the facade, so no direct async-trait dependency is required.
Registering it
Two ways, and they share one ordered stack:
App-level middleware is added to the stack first, then each plugin's contribution in topological dependency order.
Controlling order
That insertion order is just the default. Override Middleware::order(&self) -> i32 to place a middleware declaratively - lower values are outer (its before_request runs earlier and its after_response runs later). The stack is stable-sorted by order before it's installed, so a middleware lands in the right place regardless of which plugin registered it or when:
impl Middleware for SessionLoader { fn order(&self) -> i32 { -100 } // outermost: runs before auth, unwinds last // before_request / after_response …} impl Middleware for AuthGate { fn order(&self) -> i32 { -50 } // inside the session, outside the app // …}Middleware with equal order keep their registration order (app-level before plugin-level; plugins in dependency order). The default is 0.
Composition: the onion
before_request hooks run in registration order; after_response hooks run in the reverse order, so each middleware wraps the ones registered after it. With a stack [A, B, C] and a request that reaches the handler:
A.before → B.before → C.before → handler → C.after → B.after → A.afterThis onion model is what makes composition predictable: an outer middleware always sees the request first and the response last.
Short-circuiting
A before_request that returns Err(response) stops the chain: the handler and every later middleware are skipped. Only the middleware whose before_request already ran get an after_response, still in reverse. If B short-circuits in the stack above:
A.before → B.before (Err) → A.after → (response returned)C and the handler never run; A, which already processed the request, still gets to process the rejection response.
Where it sits in the layer stack
The middleware stack is installed so that umbral's own security and transport layers stay outermost: host-header validation, CORS, and compression all run before your middleware ever sees the request, and the 404 fallback is inside it (so your middleware observes misses too). You don't configure this; it's wired at App::build.
See also
- Routes - declaring the handlers your middleware wraps.
- Auth gating - the built-in login-required guard.
crates/umbral-core/src/middleware.rsfor the trait + stack implementation.