Skip to content

Requests

Every handler receives a Request. Start with input() for application data; it gives you one consistent API whether the value arrived in JSON, a form body, or the query string:

Route.post('/posts', (Request request) {
final title = request.input('title');
final page = request.integer('page', 1);
return {'title': title, 'page': page};
});

Request Information

request.method; // 'POST'
request.uri; // the full Uri
request.path; // '/api/posts'
request.ip; // the client address
request.rawBody; // the body exactly as received

Buffered bodies are limited to 10 MB by default. Set http.max_body_bytes (the skeleton reads HTTP_MAX_BODY_BYTES) to change the limit. A larger body receives 413 Payload Too Large; the limit is enforced on bytes read, even when Content-Length is absent.

Headers

request.header('content-type'); // case-insensitive
request.headers; // unmodifiable, keys lower-cased
request.bearerToken(); // the token from `Authorization: Bearer ...`, or null

Three helpers describe what the client wants:

Getter True when
isJson the request’s content-type contains json
wantsJson the accept header asks for JSON, or the path starts with /api
expectsJson either of the above

expectsJson decides whether errors are rendered as JSON or as HTML. See Error Handling.

Public URLs Behind a Proxy

request.uri describes the connection received by the Maat process. Behind a reverse proxy, use request.publicUri when generating an absolute URL so the original scheme and host can be restored:

final canonical = request.publicUri.replace(path: '/posts/42');

Forwarded headers are accepted only from addresses listed in app.trusted_proxies. Leave that setting empty unless the application is actually behind a known proxy; trusting arbitrary x-forwarded-host values can let a client influence links generated by your application.

Retrieving Input

input reads from the body first, then the query string:

request.input('title'); // null when absent
request.input('title', 'Untitled'); // with a default
request.input(); // every value, merged

Dot notation reaches into nested JSON, including through lists:

// {"author": {"name": "Ada"}, "tags": ["a", "b"]}
request.input('author.name'); // 'Ada'
request.input('tags.0'); // 'a'

Query Strings

request.query('page'); // query string only
request.query('page', '1');
request.queryAll; // Map<String, String>

Typed Accessors

request.string('title'); // String?
request.integer('page'); // int?, null when unparseable
request.integer('page', 1); // with a default
request.boolean('active'); // true for true, 1, '1', 'on', 'yes'

Numeric values are treated as true when they are non-zero.

Slices of Input

request.all(); // everything, body over query
request.only(['title', 'body']);
request.except(['_token']);

Presence

request.has('title'); // the key exists, even if its value is null
request.filled('title'); // the key exists and is not null or empty

has distinguishes “absent” from “present and null”, which matters when a client deliberately sends null to clear a field.

The JSON Body

request.json; // the decoded body: a Map, a List, or null

json is parsed once and cached. A malformed body yields null rather than throwing, so a bad request does not become a 500. A top-level JSON array is available through json, but it does not become named input for input().

merge and replace change what all() and input() return, but do not invalidate the cached json. Read input through input()/all() if anything upstream may have modified it.

Streaming a Request Body

Large uploads can bypass buffering explicitly:

Route.post('/upload', (Request request) async {
final file = File('storage/upload.bin').openWrite();
await request.bodyStream.pipe(file);
return Response.noContent();
}).streamRequestBody();

The byte limit still applies while the stream is consumed. On a streaming route, use bodyStream; rawBody, input() and json require a buffered body.

Modifying Input

request.merge({'user_id': '7'}); // add or overwrite keys
request.replace({'title': 'New'}); // discard existing input

Middleware use this to normalise input before it reaches your handler — TrimStrings is built on it.

Route Parameters

request.param('id'); // a single parameter, or null
request.params; // every parameter
request.route; // the RouteDefinition that matched

Attributes

attributes is a scratch map that travels with the request. Middleware use it to hand values to handlers further down the pipeline:

// in middleware
request.attributes['user'] = user;
// in the handler
final user = request.attributes['user'] as User;

Validating Input

final data = await request.validate({
'title': 'required|string|max:120',
'body': 'required|string',
});

validate returns only the validated keys, and throws a ValidationException — rendered as a 422 — when the input fails. It does not modify the request’s input. See Validation.

Building Requests by Hand

Request.create builds a request without a socket, which is what the test client uses:

Request.create(
method: 'POST',
path: '/api/posts?page=2',
json: {'title': 'Hello'},
);
Request.create(method: 'POST', path: '/login', form: {'email': 'a@b.c'});
Request.create(path: '/', headers: {'accept': 'application/json'});
Parameter Default Description
method 'GET' The HTTP verb.
path '/' Path, optionally with a query string.
headers {} Request headers.
body A raw body string.
json An object encoded as JSON, setting the content type.
form Form fields, URL-encoded.
ip '127.0.0.1' The client address.