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 Extractors and Responders

Most handlers do not need to parse requests or build responses by hand. ntex handles both sides of the process:

  • Extractors provide the values passed to a handler.
  • Responders turn the handler’s result into an HTTP response.

This leaves the handler free to focus on application logic.

Extractors

Handler arguments are extractors. Before calling a handler registered with Route::to(), ntex creates each argument from the request in the order it appears in the function signature. Each argument type must implement FromRequest. A handler can take up to 16 extractor arguments.

The built-in extractors cover common request data:

ExtractorReads
HttpRequestA cheap clone of the request handle
Path<T>Dynamic path segments
Query<T>URL query parameters
Json<T>A JSON request body
Form<T>A URL-encoded request body
PayloadThe streaming request body
BytesThe complete request body as bytes
StringThe complete request body as decoded text
Option<T>Some(value), or None if T fails
Result<T, T::Error>The value, or the error produced by T
(A, B, ...)Several extractors combined into one argument

For example, this handler reads a user ID from the path and an optional flag from the query string:

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

#[derive(serde::Deserialize)]
struct Options {
    details: Option<bool>,
}

async fn user(
    user_id: web::types::Path<u32>,
    options: web::types::Query<Options>,
) -> String {
    format!(
        "User {}, details: {}",
        user_id.into_inner(),
        options.details.unwrap_or(false),
    )
}

App::default().route(
    "/users/{user_id}",
    web::get().to(user),
);

If any extractor fails, the handler is not called. ntex turns the error into a response through WebResponseError.

Path<T> and Query<T> both deserialize with serde, but they represent different shapes:

  • a path tuple reads captured segments by position;
  • a path struct reads them by route variable name;
  • a query string is a set of key=value pairs, so use a struct or map rather than a tuple.

Path values are percent-decoded after the URL has been split into segments. That means an encoded slash such as %2F can appear inside one extracted value. Treat path values as input, not as safe file-system paths.

Request Bodies

A request has only one body stream, and all extractors share it. Json<T>, Form<T>, Bytes, and String consume that stream, so a handler should normally have only one body extractor. Their default in-memory limits are:

ExtractorDefault limit
Json<T>32 KiB
Form<T>16 KiB
Bytes and String256 KiB

Json<T>, Form<T>, and Query<T> use serde to deserialize values. Json<T> accepts JSON media types, including types with a +json suffix. Form<T> requires application/x-www-form-urlencoded. String decodes the body according to the request charset; Bytes leaves it unchanged.

Use JsonConfig, FormConfig, and PayloadConfig to change those limits. JsonConfig can accept additional content types. PayloadConfig can require a content type for Bytes and String; without that setting, those extractors accept any content type. FormConfig changes only the size limit.

Store extractor configuration in WebAppConfig with set_state() and install it with App::with_config():

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

fn main() {
    let config = WebAppConfig::new()
        .set_state(web::types::JsonConfig::default().limit(4096))
        .set_state(web::types::PayloadConfig::new(64 * 1024));

    let app = App::default()
        .with_config(config)
        .route("/", web::post().to(async |body: String| body));
}

Values stored with set_state() must be Send + Sync. If an application does not call with_config(), it uses the WebAppConfig from the connection’s shared configuration, and extractors fall back to their defaults when no configuration of the right type is stored.

Use Payload when the handler should process the body incrementally instead of buffering it in memory. It takes ownership of the remaining body stream and does not apply PayloadConfig; the handler decides how much data to read and how to handle it.

Optional Extractors

Sometimes invalid input should not stop the request. Wrapping an extractor in Option<T> turns a successful extraction into Some(value) and a failure into None. The failure is also logged:

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

#[derive(serde::Deserialize)]
struct Search {
    query: String,
}

async fn search(params: Option<web::types::Query<Search>>) -> String {
    match params {
        Some(params) => format!("Searching for {}", params.query),
        None => "No search query".to_owned(),
    }
}

App::default().route("/search", web::get().to(search));

If the handler needs the actual error, use Result<T, T::Error> instead. Both forms allow the handler to run when extraction fails.

These wrappers do not rewind the request body. If a wrapped body extractor reads part or all of the stream before failing, a later extractor sees only what remains. In practice, keep it as the handler’s only body extractor.

Custom Extractors

You can turn application-specific request data into a handler argument by implementing FromRequest. A custom extractor receives the application state, the HTTP request, and mutable access to the request body.

The body argument is the raw http::Payload stream. The Payload extractor in web::types is a different type: a handler argument that wraps this stream. Custom extractors use http::Payload directly:

use ntex::http::Payload;
use ntex::web::{self, App, FromRequest, HttpRequest, InternalError};

struct ClientName(String);

impl<St: web::State> FromRequest<St> for ClientName {
    type Error = InternalError<&'static str>;

    async fn from_request(_: &St, req: &HttpRequest, _: &mut Payload) -> Result<Self, Self::Error> {
        req.headers()
            .get("x-client-name")
            .and_then(|value| value.to_str().ok())
            .map(|value| ClientName(value.to_owned()))
            .ok_or_else(|| web::error::ErrorBadRequest("Missing client name"))
    }
}

async fn hello(client: ClientName) -> String {
    format!("Hello, {}!", client.0)
}

App::default().route("/", web::get().to(hello));

Handlers registered with Route::to_with_state() receive the application state and request-local state before their extractor arguments. Extractors otherwise behave in the same way. They can use application state, but they do not receive the request-local state carried by WebRequest. See Web Application State for details.

Responders

Handler return values are responders. Any type that implements Responder can be returned from a handler, and ntex turns it into an HTTP response. Responder conversion happens after the handler completes and receives both the application state and the original request.

Common responder types include:

ResponderResult
HttpResponse or HttpResponseBuilderThe response as configured
String, &String, or &'static str200 OK, text/plain; charset=utf-8
Bytes, BytesMut, or &'static [u8]200 OK, application/octet-stream
Json<T>200 OK, application/json
Form<T>200 OK, application/x-www-form-urlencoded
Option<T>The inner response, or 404 Not Found for None
Result<T, E>The inner response, or the error response produced by E
(T, StatusCode)The inner response with a different status code
Either<A, B>The response of whichever variant is returned
InternalError<T>The error response with its configured status
()200 OK with an empty body, for applications with () state

The error type in Result<T, E> must implement WebResponseError. See Web Application Errors for how errors become responses. Either is available as ntex::util::Either.

Use Responder::with_status() and Responder::with_header() when you only need to change the status or set a header:

use ntex::http::StatusCode;
use ntex::web::{self, App, Responder};

#[derive(serde::Serialize)]
struct User {
    id: u32,
}

async fn create_user() -> impl Responder {
    web::types::Json(User { id: 42 })
        .with_status(StatusCode::CREATED)
        .with_header("x-api-version", "1")
}

App::default().route(
    "/users",
    web::post().to(create_user),
);

These methods wrap the original responder. They keep its body and other headers, then apply the requested status and headers. A configured header replaces values with the same name that the inner responder produced.

For simple handlers, returning a concrete responder such as Json<T> keeps the signature clear. Use impl Responder when the concrete wrapper type is unimportant, and Either<A, B> when different branches naturally produce different responder types.

Custom Responders

Implement Responder when one of your own types should be returned directly from a handler. The conversion is asynchronous and can inspect application state or request metadata:

use ntex::http::Response;
use ntex::web::{self, App, HttpRequest, Responder};

struct Greeting(&'static str);

impl<St: web::State> Responder<St> for Greeting {
    async fn respond_to(self, _: &St, _: &HttpRequest) -> Response {
        Response::Ok()
            .content_type("text/plain; charset=utf-8")
            .body(self.0)
    }
}

async fn hello() -> Greeting {
    Greeting("Hello!")
}

App::default().route("/", web::get().to(hello));