Skip to Content
ovr

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.Part is an instance of Request.
  • Size Limits - Configurable memory and payload thresholds 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.

ts
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.form creates the multipart async iterable with the current request.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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

ts
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

ts
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)) {
		// ...
	}
});