Web Application Filters and Middleware
Like the rest of ntex, the web framework is built around Service and
Middleware. A web application is simply a service that accepts an HTTP
request, runs it through the configured pipeline, and returns a
WebResponse.
Filters and middleware let you customize this pipeline in different ways:
- A filter transforms an incoming
WebRequestbefore processing continues. - Middleware wraps a service, so it can run code both before and after that service.
Both can use application state. Middleware services receive it through
Ctx, and a filter function can take it as its first argument. For the
distinction between application state and request-local state, see
Web Application State.
Filters
App::filter(), Scope::filter(), and Resource::filter() append a
service to the inbound request pipeline. A filter accepts
WebRequest<In> and must return WebRequest<Out>:
WebRequest<In> -> filter -> WebRequest<Out>
Filters run in registration order, with each filter receiving the output of
the previous one. If a filter returns an error, ntex skips the remaining
filters, router, and handler, then renders the error through
WebResponseError.
Because request state is part of the WebRequest type, filters can enforce
requirements at compile time. For example, an authentication filter can turn
WebRequest<()> into WebRequest<AuthenticatedUser>. A handler that expects
AuthenticatedUser can only be registered after a filter that provides it:
use std::convert::Infallible;
use ntex::web::{self, WebRequest};
struct AuthenticatedUser {
id: u64,
}
async fn authenticate(
req: WebRequest<()>,
) -> Result<WebRequest<AuthenticatedUser>, Infallible> {
Ok(req.map_state(|()| AuthenticatedUser { id: 42 }))
}
async fn profile(_app: &(), user: AuthenticatedUser) -> String {
format!("User {}", user.id)
}
web::App::default()
.filter(authenticate)
.route("/profile", web::get().to_with_state(profile));
Here the application state is (), so profile receives &() as its first
argument and the AuthenticatedUser produced by the filter as its second.
WebRequest::map_state() consumes the request, transforms its request-local
state, and preserves the HTTP request and payload.
A filter can also take the application state as its first argument and return
any error that implements WebResponseError. The error stops processing and
becomes the response:
use ntex::web::{self, WebRequest, error};
fn main() {
let app = web::App::default()
.filter(async |_state: &(), req: WebRequest<()>| {
if req.headers().contains_key("x-api-key") {
Ok(req)
} else {
Err(error::ErrorForbidden("missing API key"))
}
})
.route("/", web::get().to(async || "Hello"));
}
Filters only run on the way in. They cannot inspect or modify the handler’s response. Use middleware when you need to wrap the complete request-response operation.
Filters also cannot return a successful response directly: their successful
output must be another WebRequest. Return an error to reject a request, or
use middleware when a cache hit, maintenance page, or authorization decision
should produce a response without running the inner service.
Middleware
A middleware component receives a service and wraps it with another service.
The wrapper can inspect or modify the request, call the inner service through
Ctx, and then inspect or modify the response. It can also participate in
readiness and shutdown.
Middleware can change the request-state type before calling the inner service,
but a filter is usually simpler when you only need to process the request.
Middleware can also short-circuit the pipeline by returning a WebResponse
without calling the inner service. WebRequest::into_response() is the
convenient way to keep the original request attached to that response.
Middleware can be installed with App::middleware(),
Scope::middleware(), or Resource::middleware(). Built-in middleware
can be installed directly:
use ntex::web::{self, App, middleware};
App::default()
.middleware(
middleware::DefaultHeaders::new()
.header("x-application", "example"),
)
.middleware(middleware::Logger::default())
.route("/", web::get().to(async || "Hello"));
Middleware runs in registration order on the way in. In this example,
DefaultHeaders receives the request first, followed by Logger. The response
takes the opposite path: Logger processes it first, followed by
DefaultHeaders.
DefaultHeaders only inserts a header when the response does not already have
one, so a handler can override the default. Logger writes through the log
facade; an application must install and configure a logger to see its output.
A custom middleware follows the normal ntex service model:
use ntex::http::header::{HeaderName, HeaderValue};
use ntex::web::{self, WebRequest, WebResponse};
use ntex::{Ctx, Middleware, Service};
struct ResponseHeader;
struct ResponseHeaderService<S> {
service: S,
}
impl<S, St> Middleware<S, St> for ResponseHeader {
type Service = ResponseHeaderService<S>;
fn create(&self, _state: &St, service: S) -> Self::Service {
ResponseHeaderService { service }
}
}
impl<S, St, ReqSt> Service<St, WebRequest<ReqSt>>
for ResponseHeaderService<S>
where
S: Service<St, WebRequest<ReqSt>, Res = WebResponse>,
{
type Res = WebResponse;
type Error = S::Error;
ntex::forward_ready!(St, service);
ntex::forward_shutdown!(St, service);
async fn call(
&self,
req: WebRequest<ReqSt>,
ctx: Ctx<'_, Self, St>,
) -> Result<Self::Res, Self::Error> {
let mut res = ctx.call(&self.service, req).await?;
res.headers_mut().insert(
HeaderName::from_static("x-application"),
HeaderValue::from_static("example"),
);
Ok(res)
}
}
web::App::default()
.middleware(ResponseHeader)
.route("/", web::get().to(async || "Hello"));
Middleware::create() runs when the application service is constructed. It
receives the application state and the service to wrap, and returns the
per-service wrapper. Its call() method then receives a Ctx for each
request. If the wrapper needs to retain something from application state,
clone an owned handle during create() rather than trying to retain the
borrowed &St.
Always call the inner service through Ctx::call() so the pipeline handles
readiness and lifecycle events correctly. The forward_ready! and
forward_shutdown! macros forward readiness checks and shutdown to the wrapped
service. Without them, the default implementations report the middleware as
always ready and never shut down the inner service. The macros take the state
type and a named field, so store the inner service in a named field rather
than a tuple field. See Service pipelines for more on
readiness and shutdown.
Configure Layers Before Routes
At each level, add filters and middleware before the first route, service,
configuration callback, or default service. Those registration methods move
the builder to its service-registration stage, where filter() and
middleware() are no longer available:
use ntex::web::{self, middleware, App, WebRequest};
App::default()
.middleware(middleware::Logger::default())
.filter(async |req: WebRequest<()>| {
Ok::<_, std::convert::Infallible>(req)
})
.route("/", web::get().to(async || "Hello"));
The same rule applies to Scope and Resource. See
Builder Order.
Middleware and filter calls are two separate stacks, even if their builder calls are interleaved. All middleware at a level wraps that level’s complete filter-and-router service, so it runs before every filter on the inbound path.
Where Filters and Middleware Run
You can install filters and middleware on an application, scope, or resource:
| Level | Runs for |
|---|---|
App | Every request entering the application |
Scope | Requests whose scope prefix and scope guards match |
Resource | Requests whose resource path and resource guards match |
For example, a request handled by a resource inside a scope follows this path:
application middleware
-> application filters
-> application routing and scope guards
-> scope middleware
-> scope filters
-> scope routing and resource guards
-> resource middleware
-> resource filters
-> route guards
-> route handler
The response travels back through the middleware layers in reverse. At each level, middleware runs in registration order for the request and reverse registration order for the response. Filters are not part of the response path.
The router checks a scope or resource’s own guards before entering its middleware and filters. Application filters run before the application router chooses a scope or resource, and scope filters run before the scope router chooses a nested resource. Route guards are later: the resource filter runs first, then routes and their guards are checked in registration order.
Fallback services run inside the layers of their level. When no resource
matches, the application default service runs after application middleware
and filters. When a scope matches but none of its services do, the scope
default service runs after the scope’s middleware and filters. When a resource
matches but no route does, its default service runs after resource middleware
and filters; without a custom default, it returns 405 Method Not Allowed.
See Web Applications and Routing for guard and fallback
behavior.
Errors and the Response Path
Extractor failures and errors returned by handler Result values are
converted into WebResponse values inside the route handler. Response
middleware can therefore inspect or modify those error responses normally.
Errors returned directly by filters, middleware services, or other web
services take a different path. They remain service errors while unwinding
through middleware and are rendered through WebResponseError outside the
application middleware stack. A middleware that propagates an inner error
with ? does not see the final rendered response. If it must process that
case, it needs to catch the error and convert it into a WebResponse itself.
See Web Application Errors for the error model.
Choosing Between Them
A useful rule of thumb is to use a filter for request-only work and middleware when you also need the response.
Choose a filter when:
- Only the incoming request needs processing.
- The request-local state type should change.
- Failure should stop processing before routing or handler execution.
Choose middleware when:
- Both the request and response need to be observed or modified.
- Processing may finish early with a successful response.
- Logic must wrap an entire application, scope, or resource.
- The component must control how the wrapped service’s readiness or shutdown is reported.
Filters are services too. A filter implemented as a full Service takes part
in readiness and shutdown like any other stage of the pipeline, but it cannot
see the response or the wrapped service.