Request lifecycle
An HTTP request goes through several layers before reaching the handler and just as many on the way back. Knowing in which order they run helps you decide where to put each piece (does this go in a middleware or in a guard?) and understand why an error was answered in a certain way.
Summary
- CORS: immediate response to
OPTIONSpreflights. - Body parser (its errors go straight to the global filters).
- Global middleware (
app.use_middleware,app.use_fn). - Module middleware (
CorvisModule::configure). - Static files from
ServeStaticModuleand route resolution (404if there is none). - Guards: global → controller → method.
- Interceptors, "before" part: global → controller → method.
- Pipes: global → controller → method → parameter.
- Handler.
- Interceptors, "after" part, in reverse order.
- Exception filters, if something failed: method → controller → global.
- CORS headers and writing the response.
Middleware
Global middleware runs first, in the order it was registered in the bootstrap code. Then come the middleware of the modules that implement CorvisModule::configure and whose for_routes matches the route, starting with the root module and continuing through its imports.
A middleware receives the Request and a Next, and decides whether to continue (next.run(req).await) or respond itself. What next.run returns is already the route's final response, with filters applied; the only errors that can reach it as Err are those produced before entering the route (for example, 404 Cannot GET /x).
corvis::serve_static(...) registered with app.use_middleware is just another global middleware, so it runs at its registration position. ServeStaticModule, on the other hand, is consulted after all the middleware and before resolving the route.
Guards
They run in this order and stop at the first one that returns false (a 403 Forbidden resource response) or an error:
- Global ones registered as a provider with
Provider::app_guard, in module order. - Global ones registered with
app.use_global_guards(...), in call order. - Controller ones (
#[use_guards]below#[controller]), left to right. - Method ones.
Guards run before pipes: they see the unvalidated body.
Interceptors
They wrap the rest of the chain. The entry order is the same as for guards (global providers, use_global_interceptors, controller, method), and the exit order is the reverse: the first interceptor to enter is the last to see the response. That is why an interceptor registered as a provider (Provider::app_interceptor) wraps those of use_global_interceptors; this is verified in crates/corvis/tests/enhancer_order.rs.
An interceptor can skip calling next.handle() and return its own response (this is what CacheInterceptor does on a cache hit).
Pipes
For each handler parameter (#[body], #[param], #[query], ...), the following run in a chain: the global pipes (those from Provider::app_pipe first), the controller's, the method's and finally the parameter's own (#[param("id", ParseIntPipe)]). Each pipe's output is the next one's input, and the final output is deserialized into the parameter's type.
All parameters are processed; if several fail, the error from the earliest pipe in its chain wins and, on a tie, the one from the last declared parameter.
Parameters without an attribute (types that implement FromContext) do not go through pipes.
Exception filters
If a guard, interceptor, pipe or the handler returns an error (or panics), Corvis looks for a filter in this order:
- The method's (
#[use_filters]on the handler). - The controller's.
- The global ones: first those registered with
Provider::app_filter, then those fromapp.use_global_filters(...).
The first filter whose catches(&exception) returns true is used. If none accepts it, BaseExceptionFilter responds with the standard body ({"message":...,"error":...,"statusCode":...} or {"statusCode":500,"message":"Internal server error"}).
Errors that occur outside a route (in a middleware, when resolving the route or in the body parser) only go through the global filters.
Response
The default status is 201 on POST and 200 on the rest, unless #[http_code(...)] is set. After the filters, the CORS headers are applied (those set by the handler take priority), the ETag and Content-Length on in-memory bodies, and the 304 response if the conditional request is still valid.
WebSockets
In a gateway, each message goes through guards → interceptors → pipes → handler, with the gateway's and the handler's enhancers. Global enhancers are not applied and exception filters are not supported: errors are sent to the client as an exception event. See Gateways.