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.