Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 WebRequest before 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:

LevelRuns for
AppEvery request entering the application
ScopeRequests whose scope prefix and scope guards match
ResourceRequests 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.