Multipart and Tar utilities for Web Streams
multitars is a JavaScript library providing memory-efficient parsing and production of Tar archives and `multipart/form-data` bodies, built entirely on the Web Streams API. It is currently at version 1.0.0, indicating a stable API after several pre-1.0.0 releases that focused on performance and feature additions. The library's core differentiator is its ability to process arbitrarily-sized stream data without buffering it in full, making it ideal for environments like serverless functions, browsers, and Node.js applications that require efficient handling of large binary data streams. It aims to offer Tar format support comparable to `node-tar`, including PAX headers, and provides both parsing and streaming utilities for both formats.
Common errors
-
TypeError: require is not a function
cause Attempting to use CommonJS `require()` syntax to import `multitars`.fixUse ES Module `import` syntax: `import { ... } from 'multitars';` -
Code appears to run but no data is processed / Stream hangs unexpectedly.
cause Not properly iterating the `AsyncGenerator` returned by `parseMultipart`, `untar`, `streamMultipart`, or `tar`, or failing to consume inner file streams.fixEnsure all functions returning `AsyncGenerator` are consumed with `for await...of`. For `TarFile`/`StreamFile` entries, explicitly read or drain their internal `stream` property. -
TypeError: Cannot read properties of undefined (reading 'contentType') when calling parseMultipart.
cause The `parseMultipart` function requires a `params` object with a `contentType` property.fixProvide the `contentType` parameter: `parseMultipart(stream, { contentType: 'multipart/form-data; boundary=...' });` -
TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'Uint8Array'.
cause `multitars` operates on `Uint8Array` for binary data, but a Node.js `Buffer` was provided.fixConvert `Buffer` instances to `Uint8Array` before passing them to `multitars` functions: `new Uint8Array(buffer)`.
Warnings
- gotcha This library is built exclusively for the Web Streams API and expects `Uint8Array` for binary data. Directly using Node.js `Buffer` or older Node.js `stream` interfaces will not work or will negate the memory efficiency benefits by forcing intermediate buffering.
- gotcha All parsing and streaming functions (`parseMultipart`, `streamMultipart`, `untar`, `tar`) return `AsyncGenerator`s. These must be iterated using `for await...of` loops or manually with `.next()` calls to initiate and process data. Merely calling the function does not trigger any stream operations.
- gotcha When parsing multipart or tar archives, `StreamFile`, `TarFile`, and `TarChunk` objects contain their *own* internal `ReadableStream` (accessible via the `stream` property for `TarFile`/`StreamFile`). These inner streams must be fully consumed or explicitly skipped/cancelled before the outer `AsyncGenerator` can yield the *next* file or chunk. Failing to do so will cause the outer stream to hang or not advance.
- breaking Version 1.0.0 included a patch fix for an accidental typo in the tar decoder that previously broke non-PAX GNU long name support. While a bug fix, this corrects previous incorrect behavior, which might be a breaking behavioral change for systems that inadvertently relied on the prior buggy implementation for specific tar archives.
- gotcha The `multipartContentType` utility provides a dynamically seeded boundary. While convenient for automatically generating a unique boundary for outgoing multipart messages, if your application requires a specific, static, or truly random boundary (e.g., for compatibility with external services or deterministic testing), you will need to manage boundary generation and the `Content-Type` header manually.
Install
-
npm install multitars -
yarn add multitars -
pnpm add multitars
Imports
- parseMultipart
const parseMultipart = require('multitars');import { parseMultipart } from 'multitars'; - streamMultipart
import { streamMultipart } from 'multitars/dist/streamMultipart';import { streamMultipart, FormEntry } from 'multitars'; - untar
const { untar } = require('multitars');import { untar, TarFile, TarChunk, TarTypeFlag } from 'multitars'; - tar
import { tar } from 'multitars';
Quickstart
import { parseMultipart, streamMultipart, FormEntry, untar, tar, TarTypeFlag } from 'multitars';
// --- Simulate a multipart/form-data request body ---
async function createMultipartBody() {
const entries: FormEntry[] = [
['field1', 'hello world'],
['file1', new Uint8Array([1, 2, 3, 4])],
['file2', new Blob(['another file content'], { type: 'text/plain' })],
];
const multipartStream = streamMultipart(entries);
// To get the full body as a ReadableStream, you'd typically do:
// const bodyStream = new ReadableStream({
// async pull(controller) {
// const { value, done } = await multipartStream.next();
// if (done) {
// controller.close();
// } else {
// controller.enqueue(value);
// }
// },
// });
// For quickstart, let's collect chunks to simulate a full body
const chunks: Uint8Array[] = [];
for await (const chunk of multipartStream) {
chunks.push(chunk);
}
return new ReadableStream({
start(controller) {
chunks.forEach(chunk => controller.enqueue(chunk));
controller.close();
}
});
}
async function handleMultipartRequest(requestBodyStream: ReadableStream<Uint8Array>, contentTypeHeader: string) {
console.log('--- Parsing Multipart Request ---');
for await (const entry of parseMultipart(requestBodyStream, { contentType: contentTypeHeader })) {
console.log(`Found entry: ${entry.name}, type: ${entry.type}, filename: ${entry.name}, size: ${entry.size}`);
if (entry.type === 'file') {
const fileContent = await new Response(entry.stream).arrayBuffer();
console.log(` File content length: ${fileContent.byteLength}`);
}
}
}
// --- Simulate a tar archive ---
async function createTarArchive() {
const tarEntries = [
{ name: 'hello.txt', size: 13, typeflag: TarTypeFlag.FILE, mtime: Date.now() / 1000, stream: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('Hello, Tar!\n')); controller.close(); } }) },
{ name: 'dir/', typeflag: TarTypeFlag.DIRECTORY, mtime: Date.now() / 1000 },
{ name: 'link.txt', typeflag: TarTypeFlag.SYMLINK, linkname: 'hello.txt', mtime: Date.now() / 1000 }
];
const tarStream = tar(tarEntries);
const chunks: Uint8Array[] = [];
for await (const chunk of tarStream) {
chunks.push(chunk);
}
return new ReadableStream({
start(controller) {
chunks.forEach(chunk => controller.enqueue(chunk));
controller.close();
}
});
}
async function handleTarArchive(tarBodyStream: ReadableStream<Uint8Array>) {
console.log('\n--- Untarring Archive ---');
for await (const entry of untar(tarBodyStream)) {
console.log(`Found entry: ${entry.name}, type: ${entry.typeflag === TarTypeFlag.FILE ? 'file' : entry.typeflag === TarTypeFlag.DIRECTORY ? 'directory' : 'link'}`);
if (entry.typeflag === TarTypeFlag.FILE && 'stream' in entry) {
const fileContent = await new Response(entry.stream).text();
console.log(` File content: ${fileContent.trim()}`);
}
}
}
(async () => {
// Example Usage:
const multipartRequestStream = await createMultipartBody();
const multipartBoundary = `----------${Math.random().toString(36).substring(2)}`; // Simulate a boundary
await handleMultipartRequest(multipartRequestStream, `multipart/form-data; boundary=${multipartBoundary}`);
const tarArchiveStream = await createTarArchive();
await handleTarArchive(tarArchiveStream);
})();