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:
- A shared reference to the application state.
- The request-local state.
- 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()orWebRequest::app_state(). - They must be
Send + Sync, unlike worker-local state, which may containRcandRefCell. - 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);
}