Skip to main content

MultipartReader

MultipartReader parses a multipart/form-data or multipart/mixed request body and exposes each part as a typed field or file item. It uses multipasta under the hood and writes uploaded files to the OS temp directory.

A key advantage over older busboy-based parsers: part-level headers (e.g. Content-Id, Request-Id) are fully preserved and exposed on every item via item.headers.

Package: @opra/http


Obtaining an instance

Do not construct MultipartReader directly. Obtain one from the operation context:

import { HttpContext, MultipartReader } from '@opra/http';

async upload(ctx: HttpContext) {
if (!ctx.isMultipart) return;
const reader = await ctx.getMultipartReader();
}

Methods

getAll()

getAll(): Promise<MultipartReader.Item[]>

Reads all remaining parts from the stream and returns them as an array. Waits until the entire request body has been consumed.

Use getAll() when the upload is small and bounded, or when you need all parts before starting any processing.

const parts = await reader.getAll();

for (const part of parts) {
if (part.kind === 'file') {
await store(part.storedPath, part.filename);
}
if (part.kind === 'field') {
console.log(part.field, part.value);
}
}

await reader.purge(); // delete temp files when done

getNext()

getNext(): Promise<MultipartReader.Item | undefined>

Reads and returns the next part from the stream. Returns undefined when all parts have been consumed.

Use getNext() for large uploads or when you want to start processing a part before the rest of the body has arrived.

let part: MultipartReader.Item | undefined;

while ((part = await reader.getNext())) {
if (part.kind === 'file') {
// File is already written to disk at part.storedPath.
await processFile(part.storedPath);
await part.delete(); // free disk space as soon as possible
}

if (part.kind === 'field') {
options[part.field] = part.value;
}
}
note

If a field declared as required in .MultipartContent() was never sent by the client, getNext() throws a BadRequestError after the last part is consumed and the stream closes.


cancel()

cancel(): void

Aborts stream processing. No further parts are emitted after cancel() is called. Useful when an early validation error makes reading the rest of the body unnecessary.

const part = await reader.getNext();
if (!isAllowed(part)) {
reader.cancel();
throw new ForbiddenError('Upload not permitted');
}

purge()

purge(): Promise<void>

Deletes all temporary files created during this request. By default, uploaded files are automatically deleted when the LocalFile object is garbage-collected (autoDelete: true). Call purge() to delete them immediately after you are done processing.

const parts = await reader.getAll();
try {
await processAll(parts);
} finally {
await reader.purge();
}

Properties

PropertyTypeDescription
itemsMultipartReader.Item[]All parts received so far (grows as the stream is consumed).
scopestring | undefinedValidation scope applied to field decoding.

Events

MultipartReader extends AsyncEventEmitter. Listen to events for reactive processing instead of polling with getNext().

EventPayloadEmitted when
'item'MultipartReader.ItemAny part arrives from the raw stream (before validation).
'field'MultipartReader.FieldA field part is decoded and validated (inside getNext()).
'file'MultipartReader.FileA file part passes content-type validation (inside getNext()).
'error'ErrorThe stream encounters an error or required field validation fails.
note

'item' fires as soon as a part arrives from the parser — before schema validation. 'field' and 'file' fire later, inside getNext(), after the part has been validated against your .MultipartContent() declaration. For most use cases, use getNext() or getAll() instead of listening to events directly.

reader.on('file', (file: MultipartReader.File) => {
console.log(`Received file: ${file.filename}`);
// Access part-level headers, e.g. a custom Request-Id
console.log(file.headers['x-request-id']);
});

reader.on('field', (field: MultipartReader.Field) => {
console.log(`Received field: ${field.field} = ${field.value}`);
});

reader.resume(); // start the stream when using event mode

Interfaces

MultipartReader.Field

Represents a non-file form field. The value property is decoded and validated against the type declared in .Field() on the operation.

PropertyTypeDescription
kind'field'Discriminator.
fieldstringField name as declared in .Field(fieldName, ...).
valueanyDecoded field value. Type is determined by the schema declaration.
mimeTypestring | undefinedMIME type reported by the client.
encodingstring | undefinedCharacter encoding reported by the client.
headersRecord<string, string>All part-level headers (e.g. content-disposition, custom headers).

MultipartReader.File

Represents an uploaded file. Extends LocalFile. The file is written to a temporary path on disk before this item is emitted. Calling buffer() or text() waits until the write is fully flushed.

PropertyTypeDescription
kind'file'Discriminator.
fieldstringField name as declared in .File(fieldName, ...).
storedPathstringAbsolute path to the temp file on disk.
filenamestringOriginal file name sent by the client.
typestring | undefinedMIME type reported by the client, e.g. 'image/png'.
encodingBufferEncoding | undefinedCharacter encoding of the file.
sizenumberFile size in bytes (reads fs.statSync).
autoDeletebooleanWhen true, the temp file is deleted automatically when the object is GC'd. Defaults to true.
headersRecord<string, string>All part-level headers (e.g. content-disposition, content-type, custom headers).
MethodReturnsDescription
buffer()Promise<Buffer>Reads the temp file into memory as a Buffer. Waits for the write to complete first.
text()Promise<string>Reads the temp file as a UTF-8 string. Waits for the write to complete first.
delete()Promise<void>Deletes the temp file immediately.

MultipartReader.Options

Options passed to the reader via .MultipartContent(). Uses multipasta limits instead of the former busboy configuration.

OptionTypeDescription
tempDirectorystringDirectory where uploaded files are written. Defaults to os.tmpdir().
scopestringValidation scope for field decoding.
isFile(info: PartInfo) => booleanCustom function to decide whether a part should be treated as a file. Overrides the default content-disposition heuristic.
maxPartsnumberMaximum total number of parts (fields + files).
maxTotalSizenumberMaximum combined size of all parts in bytes.
maxPartSizenumberMaximum size of a single file part in bytes.
maxFieldSizenumberMaximum size of a single field value in bytes.