Skip to content

Request bodies and responses

import 'package:gisila/gisila.dart';

The generated code calls these helpers for you based on parameter types (see parameter binding rules). Call them yourself in routes that take a raw Request, in middleware, or in hand-written Shelf handlers. All of them throw BadRequestException for missing or malformed input, which the framework renders as a 400.

Future<DecodedBody> readBody(Request request, {bool required = true})

Decodes the body according to its Content-Type.

Content-Type Returns
application/json, application/vnd.api+json, text/json Whatever jsonDecode produces.
application/x-www-form-urlencoded, multipart/form-data A FormData.
missing null when required is false, otherwise 400.
anything else 400 (Unsupported Content-Type).

When to use: when a route accepts more than one body format and you want one call that handles all of them.

Future<Object?> readJsonBody(Request request, {bool required = true})

Reads the body as JSON regardless of Content-Type. An empty body is null when required is false, otherwise a 400; malformed JSON is a 400.

When to use: JSON-only endpoints, or when you need a JSON array.

Future<FormData?> readFormBody(Request request, {bool required = true})

Reads the body strictly as url-encoded or multipart form data. Returns null only when there is no Content-Type and required is false. Any other content type is a 400.

When to use: upload endpoints and HTML form posts.

typedef DecodedBody = Object?;

Return type of readBody: a Map (JSON object), a List (JSON array), a FormData, a JSON primitive, or null.

Future<FormData> parseFormData({
required Map<String, String> headers,
required Future<String> Function() body,
required Stream<List<int>> Function() bytes,
int maxParts = defaultMaxFormParts,
int maxFieldBytes = defaultMaxFormFieldBytes,
})

The parser behind readFormBody and bindForm. It takes the pieces of a request instead of a Request, so you can test it without Shelf. Multipart bodies are capped at maxParts parts and maxFieldBytes per text field; going over either is a 400, as is a text field that is not valid UTF-8 or a multipart body without a boundary. The overall body size is capped by AppConfig.maxRequestBodyBytes / RouteConfig.maxBodyBytes.

When to use: when you need stricter limits than the defaults:

final form = await parseFormData(
headers: request.headers,
body: request.readAsString,
bytes: request.read,
maxParts: 20,
maxFieldBytes: 64 * 1024,
);
Constant Value Description
defaultMaxFormParts 1000 Default maxParts.
defaultMaxFormFieldBytes 1048576 (1 MiB) Default maxFieldBytes.
formUrlEncodedContentType ContentType('application', 'x-www-form-urlencoded') A dart:io ContentType.
multipartFormDataContentType ContentType('multipart', 'form-data') A dart:io ContentType.

The text fields and files of a form request. It is also an immutable Map<String, Object> (text fields as String, files as UploadedFile), so it works where a Map<String, dynamic> body is expected.

class FormData with MapMixin<String, Object> {
FormData({
required Map<String, String> fields,
required Map<String, UploadedFile> files,
});
Map<String, String> get fields;
Map<String, UploadedFile> get files;
}

When to use: as a controller parameter type when you need several files or a mix of text and files and don’t want a Form. Writing to it throws UnsupportedError. When a key is both a field and a file, form[key] returns the field.

@Post('/imports')
@Permissions({'data:write'})
Future<Map<String, Object?>> importCsv(FormData body, ImportService imports) async {
final mode = body.fields['mode'] ?? 'append';
final csv = body.files['rows'] ?? (throw BadRequest('Missing "rows" file'));
return imports.importRows(await csv.readAsBytes(), mode: mode);
}

One uploaded file from a multipart body.

class UploadedFile {
UploadedFile(String name, ContentType contentType, Stream<List<int>> byteStream);
final String name;
final ContentType contentType;
Future<List<int>> readAsBytes();
Stream<List<int>> openRead();
}
Member Description
name The file name the client reported. Never use it as a path without sanitising it.
contentType The part’s Content-Type (text/plain when absent or unparsable). Client-supplied.
readAsBytes() Reads the whole file into memory. The stream can be consumed only once.
openRead() The raw byte stream, for piping to disk or object storage without buffering. Single-use.

When to use: as a controller parameter named after the multipart field (UploadedFile file reads field file), or through a FileField.

@Post('/avatars/{id}')
@Permissions({'profile:write'})
Future<Map<String, Object?>> uploadAvatar(String id, UploadedFile file) async {
final bytes = await file.readAsBytes();
return {'filename': file.name, 'size': bytes.length, 'type': file.contentType.mimeType};
}
T coerce<T>(String? value, String paramName, {required bool required})

Parses a raw path, query or header string into T: String, int, double, num, bool (true/1/yes, false/0/no, any case) or DateTime (DateTime.parse), nullable or not.

  • null, or an empty string for any type but String, is missing: a 400 (Missing required parameter) when required is true and T is non-nullable, otherwise null.
  • An unparsable value is a 400 naming the parameter.
  • Any other T is a 400 (Unsupported parameter type).

When to use: reading query parameters by hand in middleware or a raw handler.

final page = coerce<int?>(request.url.queryParameters['page'], 'page', required: false) ?? 1;
Response jsonResponse({
Object? body,
int statusCode = 200,
Map<String, String>? extraHeaders,
})

JSON-encodes body and returns a response with content-type: application/json; charset=utf-8. Values are converted recursively: objects through their toJson(), DateTime as ISO 8601, enums without toJson() as their name, other objects without toJson() as toString().

When to use: in routes that return a Response so they can add headers, choose a status at runtime, or both.

@Post('/sessions')
@Public()
Future<Response> login(LoginForm form, AuthService auth) async {
final session = await auth.login(form.email.value!, form.password.value!);
return jsonResponse(
body: session.user,
extraHeaders: {'set-cookie': 'sid=${session.id}; HttpOnly; Secure; SameSite=Lax; Path=/'},
);
}
Response emptyResponse({int statusCode = 204})

An empty response with content-length: 0. Generated code uses it for void methods.