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;
}
}
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
| Property | Type | Description |
|---|---|---|
items | MultipartReader.Item[] | All parts received so far (grows as the stream is consumed). |
scope | string | undefined | Validation scope applied to field decoding. |
Events
MultipartReader extends AsyncEventEmitter. Listen to events for reactive processing instead of polling with getNext().
| Event | Payload | Emitted when |
|---|---|---|
'item' | MultipartReader.Item | Any part arrives from the raw stream (before validation). |
'field' | MultipartReader.Field | A field part is decoded and validated (inside getNext()). |
'file' | MultipartReader.File | A file part passes content-type validation (inside getNext()). |
'error' | Error | The stream encounters an error or required field validation fails. |
'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.
| Property | Type | Description |
|---|---|---|
kind | 'field' | Discriminator. |
field | string | Field name as declared in .Field(fieldName, ...). |
value | any | Decoded field value. Type is determined by the schema declaration. |
mimeType | string | undefined | MIME type reported by the client. |
encoding | string | undefined | Character encoding reported by the client. |
headers | Record<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.
| Property | Type | Description |
|---|---|---|
kind | 'file' | Discriminator. |
field | string | Field name as declared in .File(fieldName, ...). |
storedPath | string | Absolute path to the temp file on disk. |
filename | string | Original file name sent by the client. |
type | string | undefined | MIME type reported by the client, e.g. 'image/png'. |
encoding | BufferEncoding | undefined | Character encoding of the file. |
size | number | File size in bytes (reads fs.statSync). |
autoDelete | boolean | When true, the temp file is deleted automatically when the object is GC'd. Defaults to true. |
headers | Record<string, string> | All part-level headers (e.g. content-disposition, content-type, custom headers). |
| Method | Returns | Description |
|---|---|---|
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.
| Option | Type | Description |
|---|---|---|
tempDirectory | string | Directory where uploaded files are written. Defaults to os.tmpdir(). |
scope | string | Validation scope for field decoding. |
isFile | (info: PartInfo) => boolean | Custom function to decide whether a part should be treated as a file. Overrides the default content-disposition heuristic. |
maxParts | number | Maximum total number of parts (fields + files). |
maxTotalSize | number | Maximum combined size of all parts in bytes. |
maxPartSize | number | Maximum size of a single file part in bytes. |
maxFieldSize | number | Maximum size of a single field value in bytes. |