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 State

A web::App is parameterized by an application state type. It usually inherits this state from the surrounding server or service pipeline, making the same value available to handlers, extractors, filters, middleware, and error responses.

There are two kinds of state to keep in mind:

  • Application state is the state supplied by the service pipeline. A worker’s value is cloned for each connection, and handlers borrow that connection’s clone. Data that must be shared between connections belongs behind a shared handle such as Rc.
  • Request-local state belongs to one request. It is owned by WebRequest, can change type as the request moves through filters and middleware, and can be moved into a state-aware handler.

This guide explains how both kinds of state work with web::App. See Worker and request state for worker initialization and process-wide versus worker-local data, and Service state for the underlying service-state model.

The web::State Trait

To use a type as application state, implement web::State:

pub trait State: 'static {
    type Error;
}

The trait has no methods. Its associated Error type defines the application’s error domain. Handlers, filters, middleware, and fallback services use it through WebError and WebResponseError.

Most applications can use DefaultError:

use ntex::web;

#[derive(Clone)]
struct ApplicationState {
    greeting: String,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

web::State itself does not require Clone, but in practice application state used with the built-in HTTP server must implement it. ServerAppConfig::State and state passed to build_with() must be Clone, and HttpService clones the connection state while creating each connection’s application service. A non-Clone state type can still be used with an application factory directly, but it cannot be carried through these server paths.

Inheriting Worker State

web::server_with_config() creates one state value per worker and passes it to that worker’s application factory. The returned App uses the same state type:

use std::io;

use ntex::{SharedCfg, web};

/// State created independently for each worker.
#[derive(Clone)]
struct ApplicationState {
    greeting: String,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

/// Process-wide factory used to create worker state.
struct ApplicationConfig;

impl ntex::server::ServerAppConfig for ApplicationConfig {
    type State = ApplicationState;

    async fn create(&self) -> io::Result<Self::State> {
        Ok(ApplicationState {
            greeting: "Hello".to_owned(),
        })
    }
}

async fn index(state: &ApplicationState, _request_state: ()) -> String {
    state.greeting.clone()
}

#[ntex::main]
async fn main() -> io::Result<()> {
    web::server_with_config(ApplicationConfig, async |_| {
        web::App::new().route("/", web::get().to_with_state(index))
    })
    .bind("127.0.0.1:8080", SharedCfg::default())?
    .run()
    .await
}

Every request handled by a worker can access that worker’s state through the service context. Each connection receives its own clone of the worker state, so values that all connections of a worker should share, such as caches or counters, belong behind Rc. See Worker-Local and Process-Wide State for details.

Design State for Cheap Cloning

Application state is usually a small collection of handles rather than a large data structure copied for every connection:

use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use ntex::web;

#[derive(Clone)]
struct ApplicationState {
    worker_cache: Rc<RefCell<HashMap<String, String>>>,
    shared_settings: Arc<Settings>,
}

struct Settings {
    service_name: String,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

Here, connections on the same worker share worker_cache. All workers can also share the same thread-safe shared_settings value when the configuration factory gives each worker a clone of the same Arc.

Handlers receive &ApplicationState, so mutation normally happens through a client handle or an interior-mutable value such as Cell, RefCell, a lock, or an atomic. Keep RefCell borrows short and do not hold them across an .await, where unrelated work could try to borrow the same value and panic.

Accessing Application State in Handlers

Use Route::to_with_state() when a handler needs direct access to state. Resource::to_with_state() does the same for a resource route without a method guard. The handler receives the arguments in this order:

  1. A shared reference to the application state.
  2. The request-local state.
  3. Any request extractors.
use ntex::web;

#[derive(Clone)]
struct ApplicationState {
    service_name: &'static str,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

async fn user(
    state: &ApplicationState,
    _request_state: (),
    user_id: web::types::Path<u32>,
) -> String {
    format!("{} user {}", state.service_name, user_id.into_inner())
}

fn main() {
    let app = web::App::<ApplicationState>::new().route(
        "/users/{user_id}",
        web::get().to_with_state(user),
    );
}

Filters, middleware, and lower-level services access application state through their service Ctx.

Handlers registered with Route::to() receive only request extractors. Use to() when the handler does not need application or request-local state.

Extractors also receive a shared reference to application state through FromRequest, so a custom extractor can use application services or configuration. Extractors do not receive request-local state; that value is a separate handler argument supplied only by to_with_state().

Request-Local State

Application state is long-lived and shared across requests. Request-local state is different: it belongs to one request and is carried by WebRequest. Its type is the parameter in WebRequest<RequestState>.

An ordinary web request starts with () as its request-local state. Each filter or middleware can keep that value, mutate it through WebRequest::st_mut(), or replace it with another type through map_state().

This is useful for data produced while processing a request, such as authentication details. Filters and middleware can transform the value with WebRequest::map_state(), and a handler registered with to_with_state() receives the result as its second argument:

use std::convert::Infallible;
use ntex::web::{self, WebRequest};

#[derive(Clone)]
struct ApplicationState {
    service_name: &'static str,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

async fn index(state: &ApplicationState, user_id: usize) -> String {
    format!("{} user {user_id}", state.service_name)
}

fn main() {
    let app = web::App::<ApplicationState>::new()
        .filter(async |req: WebRequest<()>| {
            // Authentication could produce this request-local value.
            Ok::<_, Infallible>(req.map_state(|()| 42usize))
        })
        .route("/", web::get().to_with_state(index));
}

Request-local state moves through the filter and middleware chain. Changing the state also changes the WebRequest type expected by the next service. This lets Rust check the pipeline at compile time: a handler expecting AuthInfo, for example, can only follow a filter or middleware that produces WebRequest<AuthInfo>.

Inside filters and middleware, WebRequest::st() and WebRequest::st_mut() return the request-local state, not the application state. Application state comes from the service context instead.

map_state() consumes the previous value. If later stages need some of the old information, include it in the new state:

struct Authenticated {
    user_id: usize,
}

struct Validated {
    user: Authenticated,
    request_id: String,
}

Application filters affect every route. A scope or resource filter can create state required only by that part of the application, which keeps unrelated handlers from depending on it.

Request-local state is not stored in HttpRequest extensions, and request extractors cannot read it. Use to_with_state() when the final handler needs the value. A handler registered with to() ignores and drops it.

Typed Web Configuration Is Separate

WebAppConfig also has a typed value store populated with set_state(). Despite the similar name, these values are not the application’s service state:

  • to_with_state() does not pass them as its first argument.
  • They are read from HttpRequest::app_state() or WebRequest::app_state().
  • They must be Send + Sync, unlike worker-local state, which may contain Rc and RefCell.
  • There is one value for each concrete type.

This store is useful for HTTP-facing configuration, especially extractor limits and settings that are naturally read from a request:

use ntex::web::{self, App, HttpRequest, WebAppConfig};

struct Limits {
    max_items: usize,
}

async fn limits(req: HttpRequest) -> String {
    let max_items = req
        .app_state::<Limits>()
        .map_or(100, |limits| limits.max_items);
    format!("maximum items: {max_items}")
}

let config = WebAppConfig::new().set_state(Limits { max_items: 50 });

let app = App::default()
    .with_config(config)
    .route("/limits", web::get().to(limits));

Use application state for services and resources that participate in the service lifecycle. Use WebAppConfig values for typed web configuration.

Supplying Fixed State with .build_with()

After the first call to route(), service(), configure(), or default_service(), the application builder becomes AppServices (see Builder Order). You can finish building it with either build() or AppServices::build_with().

build() uses the state from the surrounding service pipeline. build_with(state) gives the application its own fixed state instead, allowing the outer pipeline to use a different state type.

Use build_with() when the HTTP or server pipeline has different state, or no state at all, but the web application needs its own:

use ntex::web::{self, HttpResponse};

#[derive(Clone)]
struct ApplicationState {
    greeting: &'static str,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

async fn index(state: &ApplicationState, _request_state: ()) -> HttpResponse {
    HttpResponse::Ok().body(state.greeting)
}

fn main() {
    let app = web::App::<ApplicationState>::new()
        .route("/", web::get().to_with_state(index))
        .build_with::<()>(ApplicationState {
            greeting: "Hello",
        });
}

In this standalone example, () is the outer pipeline’s state type. It is normally inferred when the application factory is passed to HttpService. The application state must implement Clone because build_with() clones it when creating application service instances.

build_with() replaces the state for the application; it does not add another state layer. Code outside the application continues to use the outer state, while web handlers and application middleware receive the fixed state.

Use server_with_config() when state needs asynchronous per-worker initialization or should be recreated with a worker. Use build_with() when a ready value should be embedded in an application factory, especially when the surrounding HTTP pipeline uses another state type.

The AppState<T> Wrapper

If the application uses DefaultError, AppState<T> saves you from writing a web::State implementation. It works with any 'static T and dereferences to the wrapped value. It also implements Clone and Default whenever T does:

use ntex::web;

#[derive(Clone)]
struct Settings {
    service_name: &'static str,
}

fn main() {
    let state = web::AppState::new(Settings {
        service_name: "users",
    });

    assert_eq!(state.service_name, "users");
    assert_eq!(state.st().service_name, "users");
}

Use a dedicated state type and implement web::State directly when you need a custom error type or other state-specific behavior.

AppState<T> does not change how values are cloned or shared. If T::clone() copies a plain field, connections receive separate copies; if it clones an Rc or Arc, they share the value behind that handle.

Testing Stateful Routes

test::init_service_st() builds an application with an explicit state value. Keep a clone of a shared handle when the test needs to inspect what the handler changed:

use std::{cell::Cell, rc::Rc};
use ntex::web::{self, test};

#[derive(Clone)]
struct ApplicationState {
    hits: Rc<Cell<usize>>,
}

impl web::State for ApplicationState {
    type Error = web::DefaultError;
}

async fn index(state: &ApplicationState, (): ()) -> &'static str {
    state.hits.set(state.hits.get() + 1);
    "ok"
}

#[ntex::test]
async fn state_is_used() {
    let state = ApplicationState {
        hits: Rc::new(Cell::new(0)),
    };

    let service = test::init_service_st(
        state.clone(),
        web::App::<ApplicationState>::new()
            .route("/", web::get().to_with_state(index)),
    )
    .await;

    let request = test::TestRequest::get().uri("/").to_request();
    let _response = test::call_service(&service, request).await;

    assert_eq!(state.hits.get(), 1);
}