File uploads
form() is a first-party body codec for multipart/form-data: bounded by the body budget (the raw body is refused at the cap before parsing), parsed by the platform’s FormData implementation (Lugas ships no MIME parser), and delivered to handlers as plain maps with native File values. See ADR-0030.
Declaring an upload route
Section titled “Declaring an upload route”import { form, json, route } from "lugas";
route({ body: form({ maxFields: 32, // text parts (default 64) maxFiles: 4, // file parts (default 16) maxFileSize: 5 * 1024 * 1024, // per-file bytes (default 10 MiB) }), handler: (ctx) => { ctx.body.fields; // Record<string, string> — text parts ctx.body.files; // Record<string, File> — native File: .name .type .size .stream() .arrayBuffer() return json(201, { saved: Object.keys(ctx.body.files) }); },});body: form(...) occupies the ordinary body slot: the manifest records the body capability, ctx.body is typed from the descriptor, and every framework error path applies (Problem Details, redaction, logging, CORS, secure headers).
Bounded consumption
Section titled “Bounded consumption”The battery’s point: uploads are the heaviest request class, so they are held to the body budget like every framework-parsed body:
- A
Content-Lengthabove the effective budget is refused before reading —413 BODY_BUDGET_EXCEEDED. - Otherwise the body is read stream-wise; crossing the cap aborts the read —
413, never a partial parse. - Only bytes within the cap ever reach a parser.
The effective budget is the usual composition: app bodyBudget default, per-route budget override, serve({ maxRequestBodySize }) ceiling that always wins.
Limits
Section titled “Limits”| Config | Default | Exceeded |
|---|---|---|
maxFields |
64 | 413 FORM_LIMIT_EXCEEDED |
maxFiles |
16 | 413 FORM_LIMIT_EXCEEDED |
maxFileSize |
10 MiB (per file) | 413 FORM_LIMIT_EXCEEDED |
Limits are positive integers validated at form() creation (LUGAS_FORM_001 on violations, including unknown keys). Byte overruns and shape overruns are both 413 — “too large” — with distinct code values (BODY_BUDGET_EXCEEDED vs FORM_LIMIT_EXCEEDED) so clients can tell which bound fired.
Failure contract
Section titled “Failure contract”| Condition | Status | Code |
|---|---|---|
| Non-multipart content type (or missing boundary) | 415 |
UNSUPPORTED_MEDIA_TYPE |
| Unparseable multipart body | 400 |
MALFORMED_MULTIPART |
| Byte budget exceeded (header or stream) | 413 |
BODY_BUDGET_EXCEEDED |
| Any form limit exceeded | 413 |
FORM_LIMIT_EXCEEDED |
None of these reach the handler — handlers never see half-parsed bodies.
Value semantics
Section titled “Value semantics”- Repeated part names collapse last-wins, mirroring
parseCookies. The nativerequest.formData()remains available in handlers when you genuinely need multiplicity. - Files are native
File—stream()forwards to storage/another service without re-serializing;arrayBuffer()for small files. - No disk-spooling config, no streaming part events (ADR-0030 non-goals): handlers get the complete bounded body; spooling for large caps is platform behavior.
Where next
Section titled “Where next”- Body limits — budgets, ceilings, and the
413boundary. - Validation — Standard Schema slots for the JSON lanes (compose: validate
ctx.body.fieldsin the handler, or parse uploads to JSON first). - Diagnostics —
LUGAS_FORM_001and the failure codes above.