Multipart

Multipart

multipart/form-data uploads are opt-in per route — the same seam as { body: true }. Declare multipart in the route options and dispatch parses the stream before the handler chain runs, so c.req stays synchronous.

import { createRouter, diskStorage, memoryStorage } from "@chttp/router";

const app = createRouter();

app.post(
  "/api/entries/:entryId/media",
  { multipart: { storage: diskStorage(MEDIA_DIR) } },
  (c) => {
    const file = c.req.file("file"); // UploadedFile | null
    const title = c.req.field("title"); // string | null
    db.run([file!.path, file!.mimeType, file!.size, title]);
    return c.json({ ok: true });
  },
);

Reading uploads

file/field are last-wins; files/fields return every part under the name.

c.req.file("avatar"); // UploadedFile | null
c.req.files("f[]"); // UploadedFile[]
c.req.field("title"); // string | null
c.req.fields("tag"); // string[]

UploadedFile:

{
  field: string; // form field name
  filename: string; // basename-sanitized
  mimeType: string;
  size: number; // bytes written
  bytes: Uint8Array | null; // memoryStorage
  path: string | null; // diskStorage (temp file the caller owns)
}

Storage

memoryStorage(); // default — buffers into UploadedFile.bytes
diskStorage(dir); // streams to dir/chttp-<ts>-<rand>-<n>, returns path

diskStorage creates dir recursively and never overwrites — temp names carry a timestamp, random hex, and a counter. On any parse failure the partial temp file is closed and unlinked. The finished file is the caller’s responsibility (move it, keep it, or delete it).

Limits

Enforced during the streaming parse — a breach answers 413 mid-upload, destroys the socket, and unlinks the partial temp file. No file is held twice, no body is re-buffered.

app.post("/up", handler, {
  multipart: {
    maxFileBytes: 100 * 1024 * 1024, // per file, default unlimited
    maxFiles: 16, // parts with a filename, default unlimited
    maxFieldBytes: 64 * 1024, // per text field, default unlimited
  },
});

Non-multipart/form-data content, a missing boundary, or a truncated body answers 400 before the handler runs. body and multipart in the same route options throw at registration — multipart owns the body.

Routes without the declaration are untouched: the stream is not consumed. The unplugin declines declared-multipart routes at build time with a warning — they stay on internal dispatch, which is what parses them.