Requests

Requests

On this page

Request is the whole of what arrived: method, URI, headers, a buffered body, parsed input, cookies, uploads, route parameters, and a typed extension map.

Handlers usually take it as Req — an alias for Arc<Request>, because the router shares one request across the extractors and the action.

pub async fn show(request: Req) -> Result<Response> {
    let slug = request.route_param("post").unwrap_or_default();
    let page: i64 = request.input("page").and_then(|p| p.parse().ok()).unwrap_or(1);
    …
}

Input

This is $request->input(), and it works the same way — merged query string and body, addressed by dotted path.

request.input("title")                    // Option<String>
request.input_or("page", "1")             // String, with a default
request.input_value("meta")               // Option<&Value>, uncoerced
request.has("title")                      // present, even if null or empty
request.filled("title")                   // present and not empty
request.all()                             // Value — query merged with body

Dotted paths reach into nested structures, and numeric segments index arrays:

request.input("user.address.city");
request.input("items.0.name");

Where the values come from

flowchart TD
    Q[query string] -->|parse_urlencoded| QV[query Value]
    B[body bytes] --> CT{content-type}
    CT -->|application/json| J[serde_json]
    CT -->|urlencoded| U[parse_urlencoded]
    CT -->|multipart| M[fields + uploads]
    CT -->|anything else| E[empty]
    J --> BV[body Value]
    U --> BV
    M --> BV
    E --> BV
    QV --> ALL["all() — body overlays query"]
    BV --> ALL

Body wins on a collision — the value someone just submitted beats the one riding in the URL.

parse_urlencoded understands PHP-style bracket syntax, so a form posting tags[]=a&tags[]=b produces an array and user[name]=Ada produces an object:

// tags[]=rust&tags[]=web  →  { "tags": ["rust", "web"] }
// user[name]=Ada          →  { "user": { "name": "Ada" } }

Typed input

let post: StorePost = request.json()?;        // from a JSON body
let login: Login = request.form()?;           // from a urlencoded body
let filters: Filters = request.query_as()?;   // from the query string

These deserialise with string coercion, so a urlencoded page=2 fills a u32 field. Without that, every form field would have to be a String and every handler would parse by hand.

For anything a client sends, prefer a request contract — it authorises, validates and filters in one step.

Rewriting input

The TrimStrings and ConvertEmptyStringsToNull middleware work through these:

request.transform_input(|value| normalise(value));
request.set_input(value);
request.merge_input(json!({ "author_id": id }));
request.input_was_rewritten();               // has anything touched it

merge_input is the idiomatic way for middleware to hand a handler something it derived.

Method, path and URI

request.method()              // &Method
request.is_method(&Method::POST)
request.uri()                 // &Uri
request.path()                // "/posts/hello"
request.query_string()        // "page=2&sort=new"
request.version()

Headers

request.header("x-request-id")        // Option<&str>
request.headers()                     // &HeaderMap
request.headers_mut()
request.content_type()                // Option<String>
request.is_json()                     // the body is JSON
request.expects_json()                // the client wants JSON back
request.bearer_token()                // Option<&str>

expects_json is the one that decides whether an error comes back as JSON or as an HTML page. It looks at Accept and at whether the request itself was JSON — an API client that forgot its Accept header still gets JSON errors.

Cookies

request.cookies()                     // &HashMap<String, String>
request.cookie("session")             // Option<&str>

Set them on the way out — see Responses.

Uploads

request.file("avatar")                // Option<&UploadedFile>
request.files("photos")               // &[UploadedFile]  (multiple under one name)
request.has_file("avatar")
let file = request.file("avatar").ok_or_else(|| Error::bad_request("no file"))?;

file.client_file_name();              // Option<&str> — untrusted
file.content_type();                  // Option<&str> — also untrusted
file.size();                          // usize
file.extension();                     // Option<String>
file.bytes();                         // &Bytes
file.store("storage/avatars/1.png")?;

client_file_name and content_type are whatever the client claimed. Never build a path from the first or trust the second — derive the extension from the bytes if it matters, and generate your own name.

Uploads are parsed from multipart/form-data at the same time the non-file fields become input, so request.input("caption") works on a multipart form.

Route parameters

request.route_param("post")           // Option<&str>
request.route_params()                // &HashMap<String, String>

The router inserts these before the route's middleware runs, so an authorisation middleware can read {post} as easily as the handler can.

Extensions

A typed, per-request map — the seam for anything middleware wants to hand downstream:

request.extensions_mut().insert(RequestId(id));
request.extension::<RequestId>();                    // Option<&T>
Request::builder().build().with_extension(value);    // builder form

This is where AuthenticatedUser<U> and MatchedRoute live. Prefer it over the container for anything belonging to one request — see Container: when not to use it.

Client IP

request.ip()                          // Option<IpAddr>

From the socket, or from X-Forwarded-For only when the server was configured with trust_forwarded_for(true). Off by default, because otherwise any client can forge the value that throttling keys on.

Building one in a test

let request = Request::builder()
    .method(Method::POST)
    .uri("/posts?draft=1")
    .header("authorization", "Bearer abc")
    .json(&json!({ "title": "Hello" }))
    .build();
Builder method
.method(m)defaults to GET
.uri(u)remember this — it defaults to /
.header(name, value)
.body(bytes)raw
.json(&value)serialises and sets content-type
.form(&[(k, v)])urlencodes and sets content-type
.route_param(name, value)as if the router had matched
.build()

Forgetting .uri() is the most common cause of a test that 404s for reasons that look mysterious.

Why bodies are buffered

The body is read fully into memory before the request exists — capped by server.max_body_bytes, 2 MiB by default.

This is a real trade and worth understanding, because it is the most visible place Rainier differs from a streaming Rust framework.

What it buys. request.input("title") is synchronous, exactly as $request->input() is in PHP. So is has, filled, file, every validation rule, and every accessor middleware uses. A lazily-streamed body would push .await into all of them — every rule, every accessor, every call site — and that is a worse framework to write applications in.

What it costs. A request bigger than the limit is a 413, and the limit applies to file uploads too. Raise it for an endpoint that needs it:

ServerOptions::default().max_body_bytes(20 * 1024 * 1024)

For genuinely large uploads — multi-gigabyte files — buffering is the wrong model and you want a dedicated endpoint that streams to storage. Rainier does not pretend otherwise.

Responses are the other way round. Body is either bytes or a stream, so a file download or an SSE endpoint does not have to fit in memory. Buffering is a statement about requests only, where the size is bounded by a limit you set and the ergonomic win is large.

This page is the framework’s docs/requests.md at 7b8cb15. Found a mistake? Edit it on GitHub.

Type to search 2.0.0 docs.