navree

Requests and extractors

Reading paths, query strings, JSON and other bodies, headers, the request id, the client IP behind trusted proxies, the current user, and body limits; writing your own extractor.

On this page

Introduction

A handler doesn't receive a request object. It declares what it needs as arguments, and an extractor builds each one from the request: Path(id) from the path, Query(filter) from the query string, Json(input) from the body, CurrentUser(user) from the authenticated principal. This is Axum's model, and every Axum extractor works in a Navree handler; Navree adds extractors for the things it manages, such as the current user, the unit of work and validated input.

src/modules/deals/notes_controller.rs

When an extractor fails, the handler doesn't run: the request ends with the extractor's error, rendered in the error format.

Path parameters

Path<T> reads the parameters of the route's path ({id}, {*rest}) into a value, a tuple or a struct. A value that doesn't parse is a 400. Routing covers the path syntax.

Query strings

Query<T> deserializes the query string into T with serde:

Rust

Missing fields must be Options or have a serde default. A value of the wrong type is a 400:

JSON

JSON bodies

Json<T> reads a JSON body into any T: Deserialize. It's Axum's Json, re-exported by navree-http and the prelude, and the same type serializes JSON responses. The request must say Content-Type: application/json:

ProblemStatusMessage
No Content-Type: application/json415Expected request with `Content-Type: application/json`
Not JSON400Failed to parse the request body as JSON: EOF while parsing an object at line 1 column 1
JSON of the wrong shape400Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 13

Axum itself answers a wrongly shaped body with a 422; Navree turns it into a 400, like every other input error.

To check the values (lengths, formats, ranges, uniqueness) and answer with a message per field, wrap the extractor: Valid(Json(input)): Valid<Json<CreateNote>>. See validation.

Other bodies

ExtractorReads
StringThe body as UTF-8 text
navree::axum::body::BytesThe raw body
navree::axum::Form<T>An application/x-www-form-urlencoded body
navree::axum::extract::MultipartA multipart/form-data body, part by part; file storage has helpers for uploads
navree::axum::extract::RequestThe whole request
Rust

An extractor that reads the body must be the handler's last argument: there is only one body. Putting it anywhere else is a compile error from Axum (the handler doesn't implement Handler).

Headers

HeaderMap gives every header:

Rust

For one header you need often, write a small extractor (see writing your own). axum-extra's TypedHeader isn't re-exported; add axum-extra to your dependencies if you want it.

The request id

Every request has an id, which ties the request log, every log line written during the request, audit entries and the response together. The pipeline keeps an incoming X-Request-Id when it's 1 to 64 letters, digits, - or _, and generates one otherwise (a UUID v7 as 32 hex digits). The response carries it in X-Request-Id, error responses included.

A handler reads it with Axum's Extension extractor:

Rust

The client IP and trusted proxies

ClientIp (from navree::http) is the address of the client making the request:

Rust

The pipeline computes it once per request and uses it everywhere: the per-IP rate limit, route throttles for guests, the request log and audit entries. It's 0.0.0.0 when the request didn't arrive over a socket.

Behind a reverse proxy, a load balancer or a backend-for-frontend, every request arrives from the proxy's address. The real client is in the X-Forwarded-For (or X-Real-IP) header the proxy adds, but anyone can send that header, so Navree only believes it from proxies you list:

config/app.toml
  • A request from a listed proxy: the client is the first address in X-Forwarded-For, reading from the right, that isn't itself a listed proxy (the left-most when all are). Without that header, X-Real-IP; without either, the proxy's own address.
  • A request from any other peer: its own address. The headers are ignored, so a client that reaches the app directly can't choose its address.

Entries are IP addresses or CIDR blocks, IPv4 or IPv6 (an IPv4-mapped IPv6 address counts as IPv4). An entry that isn't one fails the boot. From the environment, the list is comma-separated: APP_HTTP__TRUSTED_PROXIES=127.0.0.1,::1.

http.trust_proxy = true believes the headers from every peer, taking the right-most X-Forwarded-For entry. Use it only when nothing can reach the app except through a proxy that sets the header; prefer trusted_proxies, which replaces it when both are set. Both are off by default.

The current user

Two extractors give you the signed-in user. Both answer guests with a 401, so they double as a check, but the route's access declaration is what keeps guests out.

ExtractorFromGives
CurrentPrincipalnavree::httpThe Principal: the user's id and how they authenticated. No database access
CurrentUsernavree::auth, in the preludeThe user's database record (navree::auth::User), loaded once per request; also a 401 when the user no longer exists
Rust

Use CurrentPrincipal when the id is enough, and CurrentUser when you need the user's fields. For the user's roles and abilities, see the extractors on the authorization page (Abilities, Authorized, Accessible, CurrentAccess).

Client metadata

navree-auth's ClientMeta (navree::auth::ClientMeta) bundles what you record about a client, for example with a token or in an audit entry: ip (None when unknown), user_agent and request_id, all Option<String>.

Rust
ExtractorGivesCovered in
UowThe request's unit of workThe unit of work
PageQuerypage and per_page from the query stringPagination
Valid<Json<T>>, Valid<Query<T>>, Valid<Form<T>>, ValidAsync<..>Validated inputValidation
Scoped<T>A request-scoped providerProviders
Authorized<E, A>, Accessible<E, A>, Abilities, CurrentAccessThe authorized record, a list filter, the user's abilitiesAuthorization
SessionThe session, with navree-sessionSessions and CSRF

Body limits

A request body may be at most http.body_limit_bytes, 2 MiB by default. The limit is checked twice:

  • Before the handler: a Content-Length over the limit is refused with a 413 before any byte is read: "The request body must not exceed 2097152 bytes.".
  • While reading: a body sent without a length (chunked) is cut off once it passes the limit, and the extractor answers 413: "Failed to buffer the request body: length limit exceeded".
config/app.toml

A route that needs a different limit, larger for an import or smaller for a webhook, sets its own with .body_limit(bytes) on a hand-written RouteDef. The route attributes have no body-limit attribute yet, so such a route goes in a hand-written controller. File uploads have their own size and type rules on top; see file storage.

Writing your own extractor

An extractor is a type that implements Axum's FromRequestParts (or FromRequest, for one that reads the body). Use AppError as the rejection, so failures render in the error format:

Rust

An extractor that needs a service resolves it through the AppContext the pipeline puts in every request:

Rust

Request extensions are also where you find the RequestId, the ClientIp, the AuthState and the route's metadata (as Arc<RouteMeta>), if your extractor needs them. A request without X-Tenant gets:

JSON

Testing requests

TestRequest builds requests for the in-process TestClient:

Rust

Besides json, a request has query, form, body, header and bearer. Requests come from 127.0.0.1 and send Accept: application/json. See HTTP tests.

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks