@andyrmitchell/multipart-mixed
v0.1.1
Published
Streaming parser for multipart/mixed HTTP batch responses (e.g. Google batch APIs), with per-part error isolation and truncation detection.
Maintainers
Readme
@andyrmitchell/multipart-mixed
Streaming parser for multipart/mixed HTTP batch responses, such as those returned by Google's batch APIs (Gmail, Calendar, Drive…).
- Streams: each part reaches your callback as soon as its bytes arrive. A 1,000-part batch never has to sit in memory at once.
- Isolates failures per part: a malformed part is reported as
{parsed: false}, and a failed sub-request carries its own status. Neither affects the rest of the batch. - Refuses to lose data silently: a truncated response, or one whose boundary never appears, rejects instead of resolving with fewer parts.
- RFC 2046 framing: delimiters must start a line, so boundary text inside a body is safe. The preamble and epilogue are ignored, and CRLF and bare LF are both accepted.
- Zero dependencies: web-platform APIs only (
Response,ReadableStream,TextDecoder). Runs in browsers, service workers and Node 18+.
Install
npm i @andyrmitchell/multipart-mixedUsage
import { streamMultipartParts } from '@andyrmitchell/multipart-mixed';
const response = await fetch('https://www.googleapis.com/batch/gmail/v1', { method: 'POST', headers, body });
await streamMultipartParts(response, async part => {
if (!part.parsed) {
// The part's text was malformed. The rest of the batch still arrives.
console.warn(part.runtimeError?.message);
return;
}
if (part.statusCode !== 200) {
// This one sub-request failed, e.g. a 404 or 429. Its reply headers are available.
console.warn(part.contentId, part.statusCode, part.headers['retry-after']);
return;
}
await save(part.bodyData); // Record<string, unknown>: validate before reading nested fields
});Use getMultipartParts(response) if you want every part as an array. It buffers the whole batch.
What each part tells you
For a readable part (parsed: true):
| Field | Meaning |
|---|---|
| contentId | The part's Content-ID, with any <…> brackets removed. Use it to match replies to sub-requests. |
| statusCode | The embedded reply's HTTP status. |
| headers | The embedded reply's headers, keyed in lower case. |
| contentType | The embedded reply's Content-Type, parameters included. |
| bodyData | The body parsed as a JSON object. undefined if the body is not a JSON object. |
| bodyRaw | The body text, always present when there is a body. |
An unreadable part (parsed: false) carries only runtimeError, a ParseError that holds the offending raw text.
Error contract
The call rejects only when the stream as a whole cannot be trusted:
- the response has no body, or no boundary in its
Content-Type - the boundary never appears in the body
- the stream ends before the closing
--boundary--, i.e. it was truncated. Parts completed before that point have already been delivered. - your callback throws or rejects. The stream is cancelled and the call rejects with your error.
Everything else is reported per part and never rejects.
Testing helpers
@andyrmitchell/multipart-mixed/testing builds deterministic batch responses for your own tests, with no timers and no randomness:
import { buildMultipartBody, createMultipartResponse, jsonPart, responsePart, googleErrorBody } from '@andyrmitchell/multipart-mixed/testing';
const body = buildMultipartBody([
jsonPart('a', '{"id":"a"}'),
responsePart('response-b', '429 Too Many Requests', JSON.stringify(googleErrorBody(429, 'rateLimitExceeded', 'Slow down'))),
]);
const response = createMultipartResponse(body, { chunkPlan: 'byte-at-a-time' });createMultipartResponse can also simulate truncation (includeClosingDelimiter: false on the body), a missing Content-Type, zero-length chunks, a failing cancel(), and exact chunk split points.
Scope
Each part is expected to wrap an embedded HTTP reply (Content-Type: application/http), which is the shape of Google-style batch responses. Parts with no headers of their own, where the text starts directly at the status line, are also accepted.
License
MIT
