Multipart
Form data
When a user submits an HTML form with the enctype="multipart/form-data" attribute or creates a Request with form data, a multipart request is created that streams the information from the client to the server.
The platform built-in Request.formData buffers the entire request body in memory which limits scalability for very large files or requests. ovr’s Multipart parser addresses this by streaming the request body in chunks, detecting boundaries incrementally without full buffering.
- Streaming Processing - Holds one chunk in memory at a time for boundary detection. Stream massive files directly to disk, S3, or proxies without buffering.
- Compatible - Native Fetch API implementation means zero adapters for modern stacks (Deno, Bun, Hono, H3, SvelteKit, etc.). Each
Multipart.Partis an instance ofRequest. - Size Limits - Configurable
memoryandpayloadthresholds to prevent oversized requests.
Usage
To stream parts of a multipart request, construct a new Multipart async iterable, and use a for await...of loop to iterate through each part. Each Multipart.Part extends the web Request object, so all of the methods such as part.text() and part.bytes() are available to use. Additional properties provide quick access to form-specific details like the input name, filename, and content type.
import { upload } from "./upload";
import { Multipart } from "ovr";
async function fetch(req: Request) {
for await (const part of new Multipart(req)) {
part; // extends Request
part.headers; // Headers
part.body; // ReadableStream
part.name; // form input name
part.filename; // filename if available
part.type; // media type
if (part.name === "name") {
// buffer a text input
const name = await part.text();
} else if (part.name === "photo") {
// stream an upload
await upload(part.body);
} else if (part.name === "doc") {
// buffer bytes
const bytes = await part.bytes();
}
}
}
If you are using the parser within ovr app middleware,
Context.formcreates the multipart async iterable with the current request.
import { Route } from "ovr";
const post = Route.post(async (c) => {
for await (const part of c.form()) {
// ...
}
});
Data
If you do need to buffer all the data in memory, the Multipart.data method is available as a drop in replacement for Request.formData, enhanced with thresholds.
const mp = new Multipart(req, options);
const data = await mp.data(); // FormData
Options
Options are available for the maximum memory allocation, max total payload size, and max number of parts, to prevent attackers from sending massive requests.
const options: Multipart.Options = {
memory: 12 * 1024 * 1024, // increase to 12MB
payload: 1024 ** 3, // increase to 1GB
parts: 4, // only accept up to 4 parts
};
// standalone
new Multipart(request, options);
// set options for the entire app
new App({ form: options });
// Context.form sets the options for the current request
c.form(options);
Comparisons
Platform
Request.formData is a built-in method to parse form data from any request, it buffers all parts memory when called. ovr’s parser supports streaming and has memory and size guards to prevent abuse.
Remix
@remix-run/multipart-parser is a great option for multipart processing. Its search function (Boyer-Moore-Horspool) has been adapted for use in ovr. It also depends on @remix-run/headers which provides a rich API for accessing additional information about each part if needed.
Remix incrementally buffers each part in memory compared to ovr’s incremental processing of each chunk. This makes Remix unable to stream extremely large files if your server cannot hold them in memory, it requires them to be fully buffered before use.
SvelteKit
SvelteKit’s multipart parser is a full-stack solution to progressively enhance multipart submissions. It uses a custom encoding to stream files when client-side JavaScript is available. If you are using SvelteKit, it makes sense to use this parser, but it is limited to using within SvelteKit applications with client-side JS.
Busboy
busboy is the gold standard solution for multipart parsing for Node. The primary difference from ovr is that busboy is built for Node and parses an IncomingMessage instead of a Fetch API Request. Most modern frameworks are being built around the Fetch API, ovr is compatible with these frameworks without any extra conversion.
Examples
Other examples using the parser for file writes and within other frameworks.
Node
Use ovr app on a Node server to stream a file to disk.
import { createWriteStream } from "node:fs";
import { Writable } from "node:stream";
import { Route } from "ovr";
const upload = Route.post(async (c) => {
try {
for await (const part of c.form()) {
if (part.name === "photo") {
await part.body.pipeTo(
Writable.toWeb(createWriteStream(`/uploads/${part.filename}`)),
);
}
}
c.text("Upload Complete", 201);
} catch (error) {
console.error(error);
c.text("Upload Failed", 500);
}
});
Deno
Pass the Part.body directly to Deno.writeFile.
import { Route } from "ovr";
const upload = Route.post(async (c) => {
try {
for await (const part of c.form()) {
if (part.name === "photo") {
await Deno.writeFile(`/uploads/${part.filename}`, part.body);
}
}
c.text("Upload Complete", 201);
} catch (error) {
console.error(error);
c.text("Upload Failed", 500);
}
});
H3
import { H3 } from "h3";
import { Multipart } from "ovr";
const app = new H3();
app.post("/upload", async (event) => {
for await (const part of new Multipart(event.req)) {
// ...
}
});
Hono
import { Hono } from "hono";
import { Multipart } from "ovr";
const app = new Hono();
app.post("/upload", async (c) => {
for await (const part of new Multipart(c.req.raw)) {
// ...
}
});