ADR 0071 — Streaming multipart uses a Fetch-clean sequential parser
- Status: Accepted — preserves the runtime-neutral boundary of ADR 0013 and the typed HTTP surface of ADR 0005.
- Date: 2026-08-14
Context
Large multipart uploads cannot pass through Request.formData(): the runtime
materialises complete File values before application code can direct bytes to
storage. A streaming implementation must parse parts sequentially, enforce
request and per-part limits while reading, preserve file order and cancel the
active receiver when the request aborts.
The maintained @remix-run/multipart-parser package was evaluated at version
0.16.4. Its public API accepts Web streams, but its parser collects each
part’s chunks in an array and concatenates them before yielding a part. It is a
good buffered parser, not a direct-to-sink streaming boundary, so adopting it
would retain file-sized memory use under a different API.
Decision
Stitchkit keeps an isolated sequential multipart parser in the Web Fetch server
layer. It consumes ReadableStream<Uint8Array>, retains only boundary/header
look-behind state, and exposes each file part as a bounded Web stream to the
receiver registered by defineMultipartStream.
The contract descriptor remains the single source of field cardinality, request/file/text limits and declared media-type policy for buffered and streaming delivery. Streaming receivers return an application-owned value and an obligatory cleanup. Stitchkit accumulates cleanups only after receiver success and runs them exactly once in reverse order if any later parse, validation, receiver or handler phase fails. Successful handler completion transfers ownership to the application.
No Node stream, Bun type, filesystem API, object-storage client or durable transaction abstraction enters the public API.
Alternatives rejected
Request.formData()followed byFile.stream(): the complete file is already materialised before the stream exists.@remix-run/multipart-parser@0.16.4: Web-compatible, but buffers every part internally and therefore cannot satisfy direct streaming.- Node-specific parsers: violate the Fetch-clean core and create different Bun and Node behavior.
- Passing live part streams into the final handler: later text fields are not parsed or validated yet, and advancing the sequential parser requires the current part to be consumed first.
- Framework-owned filesystem/S3 adapters: storage policy and external atomicity belong to the consuming application.
Consequences
- Streaming file memory is bounded by parser look-behind and transport chunks, not total file size.
- Receivers may create external state before late text validation, so cleanup is a correctness requirement rather than an optional convenience.
- Parser behavior needs adversarial chunk-boundary, limit, abort and rollback tests on every supported runtime.
- Re-evaluating an external parser remains possible if it later exposes true per-part streaming with the same Fetch-clean guarantees.