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:
| Extractor | Reads |
|---|---|
HttpRequest | A 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 |
Payload | The streaming request body |
Bytes | The complete request body as bytes |
String | The 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=valuepairs, 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:
| Extractor | Default limit |
|---|---|
Json<T> | 32 KiB |
Form<T> | 16 KiB |
Bytes and String | 256 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:
| Responder | Result |
|---|---|
HttpResponse or HttpResponseBuilder | The response as configured |
String, &String, or &'static str | 200 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));