File uploads (multipart/form-data)
DaloyJS treats multipart/form-data as a first-class request shape. Two helpers, fileField() and multipartObject(), let you describe an upload contract once, get runtime validation (size caps, MIME allowlists, filename matchers), an end-to-end-typed handler, and a correct OpenAPI document with multipart/form-data media type and format: "binary" file fields.
Handlers receive each file as the runtime's own File or Blob, so they can pass it on with file.stream() to S3, disk, or another upstream. The request itself is read into memory first, capped at bodyLimitBytes (1 MiB by default), and then parsed with the standard formData() parser. Raise bodyLimitBytes on the upload route itself (see per-route body limits and timeouts) so the rest of the API keeps the default, and for very large files have clients upload straight to object storage with a presigned URL instead.
- 01requestmultipart/form-data bodyread up to bodyLimitBytes, parsed to File / Blob entries
- 02app capsmaxFileBytes · maxFields · maxFiles413 (size) or 400 (counts)
- 03field capsfileField(): size · MIME · magic bytesforged image/png rejected, 422
- 04handlerTyped bodybody.file.stream() to S3 or disk
Quick start
fileField() options
maxBytes: reject files larger than this many bytes.accept: MIME allowlist. Each entry can be exact ("image/png") or a wildcard ("image/*"/"*/*").filename(name): predicate for filename validation, useful for forcing extensions. When it rejects a file, the error message echoes the name with control, bidi and invisible characters removed and the length capped at 128 characters, so a crafted name cannot spoof logs or error UIs. Thenamepassed to your predicate is the raw, untouched value, after the built-in check below has accepted it.allowUnsafeFilenames: by default (since 1.16.0) a file name containing a path separator (/,\), a NUL or other control character, equal to.or.., longer than 255 characters, containing a bidirectional-override or zero-width character, named after a Windows device (CON,NUL,COM1, with or without an extension), or ending with a dot or space fails validation with a422, so a handler that joinsfile.nameonto an upload directory cannot be steered outside it (../../.ssh/authorized_keys). Browsers and curl only ever send a basename, so real clients are unaffected; a namelessBlobpasses. Settrueonly when the name is never used as a path and you need the raw value.magicBytes: verify file signatures before the handler receives the upload.truederives known signatures fromacceptfor PNG, JPEG, GIF, WebP, PDF, ZIP, and GZIP. Custom signatures support domain-specific formats.rejectScriptableImages: reject SVG, MVG, MSL, PostScript, and EPS payloads that can execute inside renderers such as ImageMagick. This is enabled automatically whenmagicBytesis configured. Set it tofalseonly when the renderer is sandboxed. The check reads a bounded prefix of the file (64 KiB since 1.23.0, previously 512 bytes) and sees through UTF-16 encoding (with or without a BOM), leading XML comments, processing instructions and DOCTYPEs, namespace-prefixed roots such as<s:svg>, and extra whitespace in MVG keywords. It also fails closed: a file whose entire probe window is XML prolog material (whitespace, comments, processing instructions, a DOCTYPE) with more bytes following is refused as unverifiable, so padding the prolog cannot push a scriptable root out of view. It is a backstop, not a parser: keepmagicByteson as the primary control, since a signature allowlist refuses every text format outright.optional: whentrue, acceptundefined/nullvalues without raising.format: OpenAPI hint, defaults to"binary".
Magic-byte verification
MIME types come from the client, so use magicBytes when the route only accepts formats with recognizable signatures. Daloy rejects a forged image/png upload whose bytes are not PNG bytes, and also rejects a file whose sniffed signature disagrees with the declared MIME type.
Strict form fields
By default, multipartObject() validates the fields you declare and drops extra form fields, so only declared fields reach the handler. Pass { strict: true } to reject undeclared fields and emit additionalProperties: false in OpenAPI. Strict mode matches field names against the shape's own keys, so a field named toString or constructor is reported as unknown.
App-level safety caps
The framework already enforces bodyLimitBytes on every request. For multipart bodies you can layer additional limits via AppOptions.multipart:
These caps are evaluated as soon as the body is parsed, so a request that exceeds them is rejected with 413 Payload Too Large (size) or 400 Bad Request (counts) before the handler runs. The multipart caps are App-wide; a per-route body cap is bodyLimitBytes on the route, and a per-route per-file cap is fileField({ maxBytes }).
OpenAPI emission
When the request body is built from multipartObject(), the OpenAPI generator emits multipart/form-data as the request body media type. Each fileField becomes { type: "string", format: "binary" } with optional x-accept, x-max-bytes, and x-magic-bytes annotations so codegen tools and humans both see the constraints.
Validation errors
Field-level failures are returned as a standard 422 Unprocessable Content problem+json document with one entry per failing field, same shape as JSON body validation, so clients have a single error path to handle.