Skip to content

multipart-parser

The multipart-parser package provides low-level streaming parsing of multipart/form-data request bodies. It works in any JavaScript runtime including Node.js, Deno, Bun, Cloudflare Workers, and browsers.

Most applications should use form-data-parser instead, which builds on this package and provides a higher-level API. Use multipart-parser directly when you need fine-grained control over how individual parts are processed.

Installation

The multipart parser is included with Remix. No additional installation is needed.

Import

ts
import {
  parseMultipart,
  parseMultipartStream,
  MultipartParser,
  isMultipartRequest,
  getMultipartBoundary,
} from 'remix/multipart-parser'

A Node.js-specific sub-export is also available:

ts
import { ... } from 'remix/multipart-parser/node'

API

isMultipartRequest(request)

Returns true if the request's Content-Type header indicates multipart/form-data.

ts
function isMultipartRequest(request: Request): boolean
ts
if (!isMultipartRequest(request)) {
  return new Response('Expected multipart form data', { status: 400 })
}

getMultipartBoundary(request)

Extracts the boundary string from the request's Content-Type header. Returns null if the header is missing or does not contain a boundary.

ts
function getMultipartBoundary(request: Request): string | null
ts
let boundary = getMultipartBoundary(request)
// e.g. '----WebKitFormBoundary7MA4YWxkTrZu0gW'

parseMultipart(body, boundary)

Parses a complete multipart body and returns an array of parts. Each part includes its headers and body as a Uint8Array.

ts
function parseMultipart(
  body: ReadableStream<Uint8Array> | Uint8Array,
  boundary: string,
): Promise<MultipartPart[]>
ts
let boundary = getMultipartBoundary(request)!
let parts = await parseMultipart(request.body!, boundary)

for (let part of parts) {
  console.log(part.headers) // Headers object
  console.log(part.body)    // Uint8Array
}

parseMultipartStream(body, boundary)

Returns an async iterator that yields parts as they are parsed from the stream. This is more memory-efficient than parseMultipart for large uploads because it processes parts one at a time.

ts
function parseMultipartStream(
  body: ReadableStream<Uint8Array>,
  boundary: string,
): AsyncIterableIterator<MultipartPart>
ts
let boundary = getMultipartBoundary(request)!

for await (let part of parseMultipartStream(request.body!, boundary)) {
  let disposition = part.headers.get('Content-Disposition')
  console.log(disposition) // 'form-data; name="file"; filename="photo.jpg"'
}

MultipartParser

A low-level class for incremental multipart parsing. Use this when you need full control over buffering and backpressure.

ts
class MultipartParser {
  constructor(boundary: string, options?: MultipartParserOptions)
  write(chunk: Uint8Array): void
  end(): void
  onPart(callback: (part: MultipartPart) => void): void
}
ts
let parser = new MultipartParser(boundary, {
  maxHeaderSize: 8192,
  maxFileSize: 10 * 1024 * 1024,
  maxParts: 100,
  maxTotalSize: 50 * 1024 * 1024,
})

parser.onPart((part) => {
  console.log(part.headers.get('Content-Disposition'))
})

for await (let chunk of request.body!) {
  parser.write(chunk)
}
parser.end()

Options

OptionTypeDefaultDescription
maxHeaderSizenumber8192Maximum size in bytes for a single part's headers.
maxFileSizenumberInfinityMaximum size in bytes for a single file part's body.
maxPartsnumberInfinityMaximum number of parts allowed in the request.
maxTotalSizenumberInfinityMaximum total size in bytes across all parts.

Error Types

The parser throws specific error types when limits are exceeded:

  • MaxHeaderSizeExceededError -- A part's headers exceeded maxHeaderSize.
  • MaxFileSizeExceededError -- A file part's body exceeded maxFileSize.
  • MaxPartsExceededError -- The number of parts exceeded maxParts.
  • MaxTotalSizeExceededError -- The total request body exceeded maxTotalSize.
ts
import {
  MaxFileSizeExceededError,
  MaxPartsExceededError,
} from 'remix/multipart-parser'

try {
  let parts = await parseMultipart(request.body!, boundary)
} catch (error) {
  if (error instanceof MaxFileSizeExceededError) {
    return new Response('File too large', { status: 413 })
  }
  if (error instanceof MaxPartsExceededError) {
    return new Response('Too many fields', { status: 400 })
  }
  throw error
}

Examples

Streaming File Upload

Process a large file upload without buffering the entire body in memory:

ts
import {
  parseMultipartStream,
  isMultipartRequest,
  getMultipartBoundary,
} from 'remix/multipart-parser'

router.map(uploadRoute, async ({ request }) => {
  if (!isMultipartRequest(request)) {
    return new Response('Bad Request', { status: 400 })
  }

  let boundary = getMultipartBoundary(request)!

  for await (let part of parseMultipartStream(request.body!, boundary)) {
    let disposition = part.headers.get('Content-Disposition')
    let filename = disposition?.match(/filename="(.+?)"/)?.[1]

    if (filename) {
      // Stream the file to storage
      await saveToStorage(filename, part.body)
    }
  }

  return new Response('Uploaded', { status: 200 })
})

With Size Limits

Protect against oversized uploads:

ts
import { parseMultipart, getMultipartBoundary } from 'remix/multipart-parser'

let parts = await parseMultipart(request.body!, getMultipartBoundary(request)!, {
  maxFileSize: 5 * 1024 * 1024,   // 5 MB per file
  maxParts: 10,                     // 10 fields max
  maxTotalSize: 20 * 1024 * 1024,  // 20 MB total
})

Released under the MIT License.