Middleware
A middleware is a function that runs before the router chooses the handler. It receives the request and a Next that represents the rest of the chain: it can inspect or modify the request, cut the chain short by returning its own response, or call next.run(req) and work on the response that comes back.
Class middleware
A middleware with dependencies is an #[injectable] that implements CorvisMiddleware:
use std::time::Instant;
use corvis::prelude::*;
/// Logs every request: `GET /api/v1/cats 200 - 3ms`.
#[injectable]
pub struct LoggerMiddleware {
#[init(Logger::new("HTTP"))]
logger: Logger,
}
#[async_trait]
impl CorvisMiddleware for LoggerMiddleware {
async fn use_(&self, req: Request, next: Next) -> HandlerResult {
let start = Instant::now();
let line = format!("{} {}", req.method(), req.url());
let res = next.run(req).await;
let status = match &res {
Ok(r) => r.status_code(),
Err(e) => e.get_status(),
};
self.logger.log(format!("{line} {status} - {}ms", start.elapsed().as_millis()));
res
}
}(examples/cats-app/src/common/middleware.rs)
The signature is:
#[async_trait]
pub trait CorvisMiddleware: Send + Sync + 'static {
async fn use_(&self, req: Request, next: Next) -> HandlerResult;
}reqarrives by value. If you want to add data for the handlers, use the request extensions:req.insert(value)and, later,req.get::<T>().next.run(req).awaitruns the next middleware or, at the end, the handler. It returns aHandlerResult(Result<Response, HttpException>).- To cut the chain short, don't call
nextand return your own response (Ok(Response::json(...))) or an error (Err(UnauthorizedException::new("..."))). - To modify the response, work on what
next.runreturns:
#[injectable]
pub struct HeaderMiddleware;
#[async_trait]
impl CorvisMiddleware for HeaderMiddleware {
async fn use_(&self, req: Request, next: Next) -> HandlerResult {
let mut res = next.run(req).await?;
res.set_header("x-middleware", "yes");
Ok(res)
}
}Difference from Express
There is no mutable res object that the middleware writes to before calling next: the response is modified on the result of next.run(req). Express's res object only exists in handlers (#[res]).
Applying middleware in a module
Class middleware is registered in the configure method of the CorvisModule trait, implemented on the module:
#[module(
imports = [/* ... */],
controllers = [AppController],
providers = [AppService],
)]
pub struct AppModule;
impl CorvisModule for AppModule {
fn configure(&self, consumer: &mut MiddlewareConsumer) {
consumer.apply::<LoggerMiddleware>().for_routes("*");
}
}#[module] detects the CorvisModule implementation on its own; it doesn't need to be declared anywhere else.
The middleware is resolved with dependency injection in the context of that module: if it isn't in providers, Corvis builds and caches it anyway, but its dependencies must be visible from the module.
MiddlewareConsumer
| Method | Use |
|---|---|
apply::<M>() | A class middleware (with DI) |
apply_instance(m) | An already built instance |
apply_fn(|req, next| async move { ... }) | A function |
All three return a configuration object with:
| Method | Use |
|---|---|
.and::<M>(), .and_instance(m), .and_fn(f) | Adds another middleware to the same rule; they run in the order written |
.exclude(routes) | Routes the rule does not cover |
.for_routes(routes) | Routes the rule covers; closes the rule and returns the consumer so you can chain another |
Route selection
for_routes and exclude accept:
- A
&strorStringwith a route pattern:"cats","cats/:id","cats/*","*". RouteInfo { path: "cats", method: RequestMethod::Get }orRouteSpec::path("cats", RequestMethod::Post), for a single verb.RouteSpec::controller::<CatsController>(): all the routes of that controller, with their final paths (global prefix and version included).- An array or
Vecof any of the above.
impl CorvisModule for CatsModule {
fn configure(&self, consumer: &mut MiddlewareConsumer) {
consumer
.apply::<HeaderMiddleware>()
.for_routes(RouteSpec::controller::<CatsController>());
consumer
.apply::<AuditMiddleware>()
.exclude(RouteInfo { path: "admin/health", method: RequestMethod::Get })
.for_routes(["admin/*", "reports"]);
}
}Pattern rules:
- The global prefix (
set_global_prefix) is added automatically: write"cats", not"api/cats". The patterns"*","(.*)","*path", and"{*path}"cover any route, with or without a prefix. :nameand{name}cover one segment.*as a segment covers zero or more segments.- A
*inside a segment ("admin*") covers the rest of the path, like/admin(.*)in Express:/api/admin,/api/ADMINpanel, and/api/admin/users/1all match. - Comparison is case-insensitive, the same rule the router uses, so a middleware can't be skipped by changing the capitalization of the URL.
URI versioning
Text patterns do not include the version segment (/v1). If the application uses URI versioning, cover the routes with RouteSpec::controller::<...>() or with a wildcard.
Functional middleware
When the middleware doesn't need dependencies, a function is enough:
consumer
.apply_fn(|req: Request, next: Next| async move {
corvis::Logger::new("Request").log(format!("{} {}", req.method(), req.path()));
next.run(req).await
})
.for_routes("*");There is also the FnMiddleware(f) type, which wraps a function as a CorvisMiddleware.
Global middleware
For a middleware that applies to all requests, register it on the application:
let mut app = CorvisFactory::create::<AppModule>().await?;
app.use_middleware(corvis::serve_static(PUBLIC_DIR)); // an instance
app.use_fn(|req, next| async move { next.run(req).await }); // a function(examples/tertulia/src/lib.rs mounts the web client's static files this way.)
use_middleware takes an instance, so it has no dependency injection. If a global middleware needs providers, register it in the root module with consumer.apply::<M>().for_routes("*").
Execution order
- CORS answers
OPTIONSpreflights before anything else. - Global middleware (
use_middleware,use_fn), in the order they were registered. - Module middleware whose rules cover the request, in the order the modules are walked and, within each one, in the order of
configure. - Router: guards, interceptors, pipes, and handler (see Request lifecycle).
Middleware runs before the route is looked up: it also runs for requests that end in a 404.
An error returned by a middleware (or a panic inside it) goes through the global filters; filters declared on a controller or a route do not see it.
Body parser errors (malformed JSON, body too large) are answered before reaching the middleware.