@ubercode/multipart-stream 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/index.cjs +930 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +785 -0
- package/dist/index.d.ts +785 -0
- package/dist/index.js +913 -0
- package/dist/index.js.map +1 -0
- package/package.json +94 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import { Readable, PassThrough } from 'stream';
|
|
2
|
+
import dicerMod from 'dicer';
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var MultipartIdleTimeoutError = class extends Error {
|
|
6
|
+
/** Stable cross-format discriminator (NFR-DR-D-007). */
|
|
7
|
+
name = "MultipartIdleTimeoutError";
|
|
8
|
+
/** The configured idle window (ms) that elapsed without source activity. */
|
|
9
|
+
idleTimeoutMs;
|
|
10
|
+
/**
|
|
11
|
+
* @param idleTimeoutMs - The configured idle window in ms.
|
|
12
|
+
* @param options - Optional `{ cause }` for wrapping a lower-level error.
|
|
13
|
+
*/
|
|
14
|
+
constructor(idleTimeoutMs, options) {
|
|
15
|
+
super(`multipart: idle timeout (${String(idleTimeoutMs)}ms)`, options);
|
|
16
|
+
this.idleTimeoutMs = idleTimeoutMs;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var MultipartTotalTimeoutError = class extends Error {
|
|
20
|
+
name = "MultipartTotalTimeoutError";
|
|
21
|
+
/** The configured total window (ms) that elapsed. */
|
|
22
|
+
totalTimeoutMs;
|
|
23
|
+
/**
|
|
24
|
+
* @param totalTimeoutMs - The configured total budget in ms.
|
|
25
|
+
* @param options - Optional `{ cause }`.
|
|
26
|
+
*/
|
|
27
|
+
constructor(totalTimeoutMs, options) {
|
|
28
|
+
super(`multipart: total timeout (${String(totalTimeoutMs)}ms)`, options);
|
|
29
|
+
this.totalTimeoutMs = totalTimeoutMs;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var MultipartAbortError = class extends Error {
|
|
33
|
+
name = "MultipartAbortError";
|
|
34
|
+
/**
|
|
35
|
+
* The signal's `reason` if the caller supplied one, else `undefined`. Per
|
|
36
|
+
* F-S-006 the library never synthesizes a reason that embeds server bytes.
|
|
37
|
+
*/
|
|
38
|
+
reason;
|
|
39
|
+
/**
|
|
40
|
+
* @param reason - Optional caller-supplied abort reason.
|
|
41
|
+
* @param options - Optional `{ cause }`.
|
|
42
|
+
*/
|
|
43
|
+
constructor(reason, options) {
|
|
44
|
+
super("multipart: operation aborted", options);
|
|
45
|
+
if (reason !== void 0) this.reason = reason;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var MultipartTruncatedError = class extends Error {
|
|
49
|
+
name = "MultipartTruncatedError";
|
|
50
|
+
/** Total bytes pulled from the source before it ended prematurely. */
|
|
51
|
+
bytesReceived;
|
|
52
|
+
/**
|
|
53
|
+
* @param bytesReceived - Cumulative bytes received before the source ended.
|
|
54
|
+
* @param options - Optional `{ cause }`.
|
|
55
|
+
*/
|
|
56
|
+
constructor(bytesReceived, options) {
|
|
57
|
+
super(
|
|
58
|
+
`multipart: stream ended before closing boundary (${String(
|
|
59
|
+
bytesReceived
|
|
60
|
+
)}B received)`,
|
|
61
|
+
options
|
|
62
|
+
);
|
|
63
|
+
this.bytesReceived = bytesReceived;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var MultipartPartTooLargeError = class extends Error {
|
|
67
|
+
name = "MultipartPartTooLargeError";
|
|
68
|
+
maxPartBytes;
|
|
69
|
+
partIndex;
|
|
70
|
+
bytesReceived;
|
|
71
|
+
/**
|
|
72
|
+
* @param info - Structured trip info: `{ maxPartBytes, partIndex, bytesReceived }`.
|
|
73
|
+
* @param options - Optional `{ cause }`.
|
|
74
|
+
*/
|
|
75
|
+
constructor(info, options) {
|
|
76
|
+
super(
|
|
77
|
+
`multipart: part ${String(info.partIndex)} exceeded maxPartBytes (${String(
|
|
78
|
+
info.maxPartBytes
|
|
79
|
+
)}) at ${String(info.bytesReceived)}B`,
|
|
80
|
+
options
|
|
81
|
+
);
|
|
82
|
+
this.maxPartBytes = info.maxPartBytes;
|
|
83
|
+
this.partIndex = info.partIndex;
|
|
84
|
+
this.bytesReceived = info.bytesReceived;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var MultipartHeadersTooLargeError = class extends Error {
|
|
88
|
+
name = "MultipartHeadersTooLargeError";
|
|
89
|
+
limit;
|
|
90
|
+
partIndex;
|
|
91
|
+
cap;
|
|
92
|
+
observed;
|
|
93
|
+
/**
|
|
94
|
+
* @param info - Structured trip info: `{ limit, partIndex, cap, observed }`.
|
|
95
|
+
* @param options - Optional `{ cause }`.
|
|
96
|
+
*/
|
|
97
|
+
constructor(info, options) {
|
|
98
|
+
super(
|
|
99
|
+
`multipart: part ${String(info.partIndex)} headers exceeded ${info.limit} cap (${String(
|
|
100
|
+
info.cap
|
|
101
|
+
)}) at ${String(info.observed)}`,
|
|
102
|
+
options
|
|
103
|
+
);
|
|
104
|
+
this.limit = info.limit;
|
|
105
|
+
this.partIndex = info.partIndex;
|
|
106
|
+
this.cap = info.cap;
|
|
107
|
+
this.observed = info.observed;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var MultipartTooManyPartsError = class extends Error {
|
|
111
|
+
name = "MultipartTooManyPartsError";
|
|
112
|
+
maxParts;
|
|
113
|
+
observed;
|
|
114
|
+
/**
|
|
115
|
+
* @param info - Structured trip info: `{ maxParts, observed }`.
|
|
116
|
+
* @param options - Optional `{ cause }`.
|
|
117
|
+
*/
|
|
118
|
+
constructor(info, options) {
|
|
119
|
+
super(
|
|
120
|
+
`multipart: envelope exceeded maxParts (${String(info.maxParts)}) at part ${String(
|
|
121
|
+
info.observed
|
|
122
|
+
)}`,
|
|
123
|
+
options
|
|
124
|
+
);
|
|
125
|
+
this.maxParts = info.maxParts;
|
|
126
|
+
this.observed = info.observed;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/internal/format-error-embed.ts
|
|
131
|
+
var FORMAT_ERROR_EMBED_LIMIT = 120;
|
|
132
|
+
var ELLIPSIS = "\u2026";
|
|
133
|
+
var REDACTION_TOKEN = "[redacted-control]";
|
|
134
|
+
var ANSI_ESCAPE_RE = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
|
|
135
|
+
var CONTROL_CHARS_RE = /[\x00-\x1F\x7F]/g;
|
|
136
|
+
function redactControlAndAnsi(s) {
|
|
137
|
+
return s.replace(ANSI_ESCAPE_RE, REDACTION_TOKEN).replace(CONTROL_CHARS_RE, REDACTION_TOKEN);
|
|
138
|
+
}
|
|
139
|
+
function truncateForErrorEmbed(value) {
|
|
140
|
+
const s = typeof value === "string" ? value : String(value);
|
|
141
|
+
const redacted = redactControlAndAnsi(s);
|
|
142
|
+
const stringified = JSON.stringify(redacted);
|
|
143
|
+
if (stringified.length > FORMAT_ERROR_EMBED_LIMIT) {
|
|
144
|
+
return stringified.slice(0, FORMAT_ERROR_EMBED_LIMIT) + ELLIPSIS;
|
|
145
|
+
}
|
|
146
|
+
return stringified;
|
|
147
|
+
}
|
|
148
|
+
function summarizeError(err) {
|
|
149
|
+
if (err instanceof Error) {
|
|
150
|
+
return {
|
|
151
|
+
name: err.name,
|
|
152
|
+
message: truncateForErrorEmbed(err.message)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
name: "Error",
|
|
157
|
+
message: truncateForErrorEmbed(err)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/extract-boundary.ts
|
|
162
|
+
function extractBoundary(contentTypeHeader) {
|
|
163
|
+
if (contentTypeHeader == null || contentTypeHeader === "") {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"multipart: Content-Type header is required to extract boundary"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const header = contentTypeHeader;
|
|
169
|
+
const len = header.length;
|
|
170
|
+
let i = 0;
|
|
171
|
+
while (i < len && header.charCodeAt(i) !== 59) i++;
|
|
172
|
+
while (i < len) {
|
|
173
|
+
while (i < len) {
|
|
174
|
+
const c = header.charCodeAt(i);
|
|
175
|
+
if (c === 59 || c === 32 || c === 9) {
|
|
176
|
+
i++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (i >= len) break;
|
|
182
|
+
const nameStart = i;
|
|
183
|
+
while (i < len) {
|
|
184
|
+
const c = header.charCodeAt(i);
|
|
185
|
+
if (c === 61 || c === 59) break;
|
|
186
|
+
if (c === 32 || c === 9) break;
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
const name = header.slice(nameStart, i).toLowerCase();
|
|
190
|
+
while (i < len) {
|
|
191
|
+
const c = header.charCodeAt(i);
|
|
192
|
+
if (c === 32 || c === 9) {
|
|
193
|
+
i++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
if (i >= len || header.charCodeAt(i) !== 61) {
|
|
199
|
+
while (i < len && header.charCodeAt(i) !== 59) i++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
i++;
|
|
203
|
+
while (i < len) {
|
|
204
|
+
const c = header.charCodeAt(i);
|
|
205
|
+
if (c === 32 || c === 9) {
|
|
206
|
+
i++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
let value = "";
|
|
212
|
+
if (i < len && header.charCodeAt(i) === 34) {
|
|
213
|
+
i++;
|
|
214
|
+
const buf = [];
|
|
215
|
+
while (i < len) {
|
|
216
|
+
const c = header.charCodeAt(i);
|
|
217
|
+
if (c === 92 && i + 1 < len) {
|
|
218
|
+
buf.push(header.charAt(i + 1));
|
|
219
|
+
i += 2;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (c === 34) {
|
|
223
|
+
i++;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
buf.push(header.charAt(i));
|
|
227
|
+
i++;
|
|
228
|
+
}
|
|
229
|
+
value = buf.join("");
|
|
230
|
+
} else {
|
|
231
|
+
const valStart = i;
|
|
232
|
+
while (i < len) {
|
|
233
|
+
const c = header.charCodeAt(i);
|
|
234
|
+
if (c === 59 || c === 32 || c === 9) break;
|
|
235
|
+
i++;
|
|
236
|
+
}
|
|
237
|
+
value = header.slice(valStart, i);
|
|
238
|
+
}
|
|
239
|
+
if (name === "boundary") {
|
|
240
|
+
if (value === "") {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`multipart: boundary parameter is empty in Content-Type: ${truncateForErrorEmbed(
|
|
243
|
+
header
|
|
244
|
+
)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
while (i < len && header.charCodeAt(i) !== 59) i++;
|
|
250
|
+
}
|
|
251
|
+
throw new Error(
|
|
252
|
+
`multipart: boundary parameter missing from Content-Type: ${truncateForErrorEmbed(
|
|
253
|
+
header
|
|
254
|
+
)}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/internal/default-logger.ts
|
|
259
|
+
var defaultLogger = (event) => {
|
|
260
|
+
if (event.meta === void 0) {
|
|
261
|
+
console.warn(event.msg);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
console.warn(event.msg, event.meta);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/internal/validate-timeout.ts
|
|
268
|
+
var MAX_SAFE_TIMEOUT_MS = 2147483647;
|
|
269
|
+
function validatePositiveTimeout(name, value) {
|
|
270
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > MAX_SAFE_TIMEOUT_MS) {
|
|
271
|
+
const observed = typeof value === "number" ? String(value) : typeof value;
|
|
272
|
+
throw new TypeError(
|
|
273
|
+
`multipart: ${name} must be a positive finite integer in [1, 2_147_483_647]; got ${observed}. Note: Node's setTimeout clamps values above 2^31-1 to 1ms, so larger timeouts are silently broken.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/internal/flatten-headers.ts
|
|
279
|
+
function flattenHeaderValue(v) {
|
|
280
|
+
if (v == null) return "";
|
|
281
|
+
if (typeof v === "string") return v;
|
|
282
|
+
if (Buffer.isBuffer(v)) return v.toString("utf8");
|
|
283
|
+
if (Array.isArray(v)) {
|
|
284
|
+
const parts = [];
|
|
285
|
+
for (const inner of v) {
|
|
286
|
+
const flat = flattenHeaderValue(inner);
|
|
287
|
+
if (flat !== "") parts.push(flat);
|
|
288
|
+
}
|
|
289
|
+
return parts.join(", ");
|
|
290
|
+
}
|
|
291
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
292
|
+
return String(v);
|
|
293
|
+
}
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
function flattenDicerHeaders(raw) {
|
|
297
|
+
if (raw == null) return {};
|
|
298
|
+
const out = {};
|
|
299
|
+
for (const key of Object.keys(raw)) {
|
|
300
|
+
const lower = key.toLowerCase();
|
|
301
|
+
const value = flattenHeaderValue(raw[key]);
|
|
302
|
+
out[lower] = value;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
function looksLikeResponse(input) {
|
|
307
|
+
if (typeof Response !== "undefined" && input instanceof Response) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (input == null || typeof input !== "object") return false;
|
|
311
|
+
const candidate = input;
|
|
312
|
+
return typeof candidate.headers === "object" && candidate.headers !== null && typeof candidate.headers.get === "function" && "body" in candidate;
|
|
313
|
+
}
|
|
314
|
+
function looksLikeReadable(input) {
|
|
315
|
+
if (input == null || typeof input !== "object") return false;
|
|
316
|
+
const candidate = input;
|
|
317
|
+
return typeof candidate.pipe === "function" && typeof candidate.on === "function";
|
|
318
|
+
}
|
|
319
|
+
function normalizeInput(input, opts) {
|
|
320
|
+
if (looksLikeResponse(input)) {
|
|
321
|
+
if (input.body == null) {
|
|
322
|
+
throw new Error("multipart: response body is null");
|
|
323
|
+
}
|
|
324
|
+
const contentType = input.headers.get("content-type");
|
|
325
|
+
const boundary = extractBoundary(contentType);
|
|
326
|
+
const webBody = input.body;
|
|
327
|
+
const readable = Readable.fromWeb(webBody);
|
|
328
|
+
return { kind: "response", readable, boundary };
|
|
329
|
+
}
|
|
330
|
+
if (looksLikeReadable(input)) {
|
|
331
|
+
if (typeof opts.boundary !== "string" || opts.boundary === "") {
|
|
332
|
+
throw new Error(
|
|
333
|
+
"multipart: boundary option is required when input is a Readable"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return { kind: "readable", readable: input, boundary: opts.boundary };
|
|
337
|
+
}
|
|
338
|
+
throw new Error(
|
|
339
|
+
"multipart: input must be a Response or a Node Readable"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/internal/queue-notifier.ts
|
|
344
|
+
function createQueueNotifier() {
|
|
345
|
+
const items = [];
|
|
346
|
+
let waiter = null;
|
|
347
|
+
let ended = false;
|
|
348
|
+
const push = (item) => {
|
|
349
|
+
if (ended && item.type !== "end" && item.type !== "error") {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (item.type === "end") {
|
|
353
|
+
if (ended) return;
|
|
354
|
+
ended = true;
|
|
355
|
+
}
|
|
356
|
+
if (waiter) {
|
|
357
|
+
const w = waiter;
|
|
358
|
+
waiter = null;
|
|
359
|
+
w(item);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
items.push(item);
|
|
363
|
+
};
|
|
364
|
+
const signalEnd = () => {
|
|
365
|
+
push({ type: "end" });
|
|
366
|
+
};
|
|
367
|
+
const signalError = (err) => {
|
|
368
|
+
push({ type: "error", err });
|
|
369
|
+
};
|
|
370
|
+
const next = () => {
|
|
371
|
+
const buffered = items.shift();
|
|
372
|
+
if (buffered !== void 0) {
|
|
373
|
+
return Promise.resolve(buffered);
|
|
374
|
+
}
|
|
375
|
+
return new Promise((resolve) => {
|
|
376
|
+
waiter = resolve;
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
const drainPendingParts = () => {
|
|
380
|
+
const parts = [];
|
|
381
|
+
const remaining = [];
|
|
382
|
+
for (const item of items) {
|
|
383
|
+
if (item.type === "part") {
|
|
384
|
+
parts.push(item.part);
|
|
385
|
+
} else {
|
|
386
|
+
remaining.push(item);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
items.length = 0;
|
|
390
|
+
items.push(...remaining);
|
|
391
|
+
return parts;
|
|
392
|
+
};
|
|
393
|
+
return {
|
|
394
|
+
push,
|
|
395
|
+
signalEnd,
|
|
396
|
+
signalError,
|
|
397
|
+
next,
|
|
398
|
+
get pending() {
|
|
399
|
+
return items;
|
|
400
|
+
},
|
|
401
|
+
drainPendingParts
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/internal/timers.ts
|
|
406
|
+
function setupTimers(opts, startMs) {
|
|
407
|
+
const controller = new AbortController();
|
|
408
|
+
const callerSignal = opts.signal;
|
|
409
|
+
let storedError;
|
|
410
|
+
let cleaned = false;
|
|
411
|
+
let idleTimer;
|
|
412
|
+
let totalTimer;
|
|
413
|
+
const abortInternally = (err) => {
|
|
414
|
+
if (controller.signal.aborted) return;
|
|
415
|
+
storedError = err;
|
|
416
|
+
controller.abort(err);
|
|
417
|
+
runCleanup();
|
|
418
|
+
};
|
|
419
|
+
const onCallerAbort = () => {
|
|
420
|
+
abortInternally(new MultipartAbortError(callerSignal?.reason));
|
|
421
|
+
};
|
|
422
|
+
const onIdle = () => {
|
|
423
|
+
abortInternally(new MultipartIdleTimeoutError(opts.idleTimeoutMs));
|
|
424
|
+
};
|
|
425
|
+
const onTotal = () => {
|
|
426
|
+
abortInternally(new MultipartTotalTimeoutError(opts.totalTimeoutMs));
|
|
427
|
+
};
|
|
428
|
+
function runCleanup() {
|
|
429
|
+
if (cleaned) return;
|
|
430
|
+
cleaned = true;
|
|
431
|
+
if (idleTimer !== void 0) {
|
|
432
|
+
clearTimeout(idleTimer);
|
|
433
|
+
idleTimer = void 0;
|
|
434
|
+
}
|
|
435
|
+
if (totalTimer !== void 0) {
|
|
436
|
+
clearTimeout(totalTimer);
|
|
437
|
+
totalTimer = void 0;
|
|
438
|
+
}
|
|
439
|
+
if (callerSignal !== void 0) {
|
|
440
|
+
callerSignal.removeEventListener("abort", onCallerAbort);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const resetIdle = () => {
|
|
444
|
+
if (cleaned || controller.signal.aborted) return;
|
|
445
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
446
|
+
idleTimer = setTimeout(onIdle, opts.idleTimeoutMs);
|
|
447
|
+
};
|
|
448
|
+
if (callerSignal?.aborted) {
|
|
449
|
+
storedError = new MultipartAbortError(callerSignal.reason);
|
|
450
|
+
controller.abort(storedError);
|
|
451
|
+
} else {
|
|
452
|
+
if (callerSignal !== void 0) {
|
|
453
|
+
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
454
|
+
}
|
|
455
|
+
idleTimer = setTimeout(onIdle, opts.idleTimeoutMs);
|
|
456
|
+
totalTimer = setTimeout(onTotal, opts.totalTimeoutMs);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
signal: controller.signal,
|
|
460
|
+
resetIdle,
|
|
461
|
+
cleanup: runCleanup,
|
|
462
|
+
abortError: () => storedError
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/parse-multipart-related.ts
|
|
467
|
+
function measureHeaderValueBytes(value) {
|
|
468
|
+
if (typeof value === "string") return Buffer.byteLength(value);
|
|
469
|
+
if (Array.isArray(value)) {
|
|
470
|
+
let total = 0;
|
|
471
|
+
for (const inner of value) {
|
|
472
|
+
total += measureHeaderValueBytes(inner);
|
|
473
|
+
}
|
|
474
|
+
return total;
|
|
475
|
+
}
|
|
476
|
+
if (Buffer.isBuffer(value)) return value.length;
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
var Dicer = dicerMod.default ?? dicerMod;
|
|
480
|
+
function parseMultipartRelated(input, opts) {
|
|
481
|
+
return parseMultipartRelatedImpl(input, opts);
|
|
482
|
+
}
|
|
483
|
+
async function* parseMultipartRelatedImpl(input, opts) {
|
|
484
|
+
validatePositiveTimeout("idleTimeoutMs", opts.idleTimeoutMs);
|
|
485
|
+
validatePositiveTimeout("totalTimeoutMs", opts.totalTimeoutMs);
|
|
486
|
+
const maxPartBytes = opts.maxPartBytes;
|
|
487
|
+
const maxParts = opts.maxParts ?? 1e4;
|
|
488
|
+
const maxHeadersPerPart = opts.maxHeadersPerPart ?? 100;
|
|
489
|
+
const maxHeaderBytesPerPart = opts.maxHeaderBytesPerPart ?? 16384;
|
|
490
|
+
const { readable: source, boundary } = normalizeInput(input, {
|
|
491
|
+
boundary: opts.boundary
|
|
492
|
+
});
|
|
493
|
+
const logger = opts.logger ?? defaultLogger;
|
|
494
|
+
const dicer = new Dicer({ boundary });
|
|
495
|
+
const queue = createQueueNotifier();
|
|
496
|
+
let bytesReceived = 0;
|
|
497
|
+
let nextPartIndex = 0;
|
|
498
|
+
let dicerFinished = false;
|
|
499
|
+
let cleaned = false;
|
|
500
|
+
let abortPushed = false;
|
|
501
|
+
const allPartStreams = /* @__PURE__ */ new Set();
|
|
502
|
+
const startMs = Date.now();
|
|
503
|
+
const timers = setupTimers(
|
|
504
|
+
{
|
|
505
|
+
idleTimeoutMs: opts.idleTimeoutMs,
|
|
506
|
+
totalTimeoutMs: opts.totalTimeoutMs,
|
|
507
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {}
|
|
508
|
+
});
|
|
509
|
+
if (timers.signal.aborted) {
|
|
510
|
+
const abortErr = timers.abortError() ?? new MultipartAbortError(opts.signal?.reason);
|
|
511
|
+
queue.signalError(abortErr);
|
|
512
|
+
abortPushed = true;
|
|
513
|
+
}
|
|
514
|
+
const onCombinedAbort = () => {
|
|
515
|
+
if (cleaned || abortPushed) return;
|
|
516
|
+
abortPushed = true;
|
|
517
|
+
const abortErr = timers.abortError() ?? new MultipartAbortError(opts.signal?.reason);
|
|
518
|
+
queue.signalError(abortErr);
|
|
519
|
+
};
|
|
520
|
+
if (!timers.signal.aborted) {
|
|
521
|
+
timers.signal.addEventListener("abort", onCombinedAbort, { once: true });
|
|
522
|
+
}
|
|
523
|
+
const fireProgress = () => {
|
|
524
|
+
const callback = opts.onProgress;
|
|
525
|
+
if (callback === void 0) return;
|
|
526
|
+
const elapsedMs = Date.now() - startMs;
|
|
527
|
+
const rateBps = elapsedMs <= 0 ? 0 : Math.round(bytesReceived * 1e3 / elapsedMs);
|
|
528
|
+
try {
|
|
529
|
+
callback({ bytes: bytesReceived, elapsedMs, rateBps });
|
|
530
|
+
} catch (err) {
|
|
531
|
+
logger({
|
|
532
|
+
level: "warn",
|
|
533
|
+
msg: "multipart: onProgress threw",
|
|
534
|
+
meta: { errSummary: summarizeError(err) }
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
let partsObserved = 0;
|
|
539
|
+
const onPart = (partStream) => {
|
|
540
|
+
allPartStreams.add(partStream);
|
|
541
|
+
const partIndex = nextPartIndex++;
|
|
542
|
+
partsObserved += 1;
|
|
543
|
+
if (partsObserved > maxParts) {
|
|
544
|
+
if (!partStream.destroyed) partStream.destroy();
|
|
545
|
+
queue.signalError(
|
|
546
|
+
new MultipartTooManyPartsError({
|
|
547
|
+
maxParts,
|
|
548
|
+
observed: partsObserved
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const headersAccumulator = {
|
|
554
|
+
value: {}
|
|
555
|
+
};
|
|
556
|
+
const onHeader = (raw) => {
|
|
557
|
+
const bag = raw;
|
|
558
|
+
let headerCount = 0;
|
|
559
|
+
let headerBytes = 0;
|
|
560
|
+
if (bag != null) {
|
|
561
|
+
for (const name of Object.keys(bag)) {
|
|
562
|
+
const value = bag[name];
|
|
563
|
+
const nameBytes = Buffer.byteLength(name);
|
|
564
|
+
const values = Array.isArray(value) ? value : [value];
|
|
565
|
+
for (const inner of values) {
|
|
566
|
+
headerCount += 1;
|
|
567
|
+
headerBytes += nameBytes + 4 + measureHeaderValueBytes(inner);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (headerCount > maxHeadersPerPart) {
|
|
572
|
+
if (!partStream.destroyed) partStream.destroy();
|
|
573
|
+
queue.signalError(
|
|
574
|
+
new MultipartHeadersTooLargeError({
|
|
575
|
+
limit: "count",
|
|
576
|
+
partIndex,
|
|
577
|
+
cap: maxHeadersPerPart,
|
|
578
|
+
observed: headerCount
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (headerBytes > maxHeaderBytesPerPart) {
|
|
584
|
+
if (!partStream.destroyed) partStream.destroy();
|
|
585
|
+
queue.signalError(
|
|
586
|
+
new MultipartHeadersTooLargeError({
|
|
587
|
+
limit: "bytes",
|
|
588
|
+
partIndex,
|
|
589
|
+
cap: maxHeaderBytesPerPart,
|
|
590
|
+
observed: headerBytes
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
headersAccumulator.value = flattenDicerHeaders(
|
|
596
|
+
raw
|
|
597
|
+
);
|
|
598
|
+
const headers = headersAccumulator.value;
|
|
599
|
+
const contentType = headers["content-type"] ?? "";
|
|
600
|
+
const contentId = headers["content-id"];
|
|
601
|
+
const contentLengthRaw = headers["content-length"];
|
|
602
|
+
const parsedLen = contentLengthRaw !== void 0 ? Number.parseInt(contentLengthRaw, 10) : Number.NaN;
|
|
603
|
+
const contentLength = Number.isFinite(parsedLen) ? parsedLen : void 0;
|
|
604
|
+
let publicBody = partStream;
|
|
605
|
+
if (maxPartBytes !== void 0) {
|
|
606
|
+
const cap = maxPartBytes;
|
|
607
|
+
let partBytesAccumulated = 0;
|
|
608
|
+
let tripped = false;
|
|
609
|
+
const counter = new PassThrough();
|
|
610
|
+
allPartStreams.add(counter);
|
|
611
|
+
const onUpstreamData = (chunk) => {
|
|
612
|
+
if (tripped) return;
|
|
613
|
+
partBytesAccumulated += chunk.length;
|
|
614
|
+
if (partBytesAccumulated > cap) {
|
|
615
|
+
tripped = true;
|
|
616
|
+
if (!partStream.destroyed) partStream.destroy();
|
|
617
|
+
queue.signalError(
|
|
618
|
+
new MultipartPartTooLargeError({
|
|
619
|
+
maxPartBytes: cap,
|
|
620
|
+
partIndex,
|
|
621
|
+
bytesReceived: partBytesAccumulated
|
|
622
|
+
})
|
|
623
|
+
);
|
|
624
|
+
if (!counter.writableEnded) counter.end();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (counter.writable && !counter.writableEnded) {
|
|
628
|
+
counter.write(chunk);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
const onUpstreamEnd = () => {
|
|
632
|
+
if (tripped) return;
|
|
633
|
+
if (!counter.writableEnded) counter.end();
|
|
634
|
+
};
|
|
635
|
+
const onUpstreamError = (err) => {
|
|
636
|
+
if (!counter.destroyed) counter.destroy(err);
|
|
637
|
+
};
|
|
638
|
+
partStream.on("data", onUpstreamData);
|
|
639
|
+
partStream.on("end", onUpstreamEnd);
|
|
640
|
+
partStream.on("error", onUpstreamError);
|
|
641
|
+
publicBody = counter;
|
|
642
|
+
}
|
|
643
|
+
const part = {
|
|
644
|
+
index: partIndex,
|
|
645
|
+
boundary,
|
|
646
|
+
headers,
|
|
647
|
+
rawHeaders: Buffer.alloc(0),
|
|
648
|
+
contentType,
|
|
649
|
+
...contentId !== void 0 ? { contentId } : {},
|
|
650
|
+
...contentLength !== void 0 ? { contentLength } : {},
|
|
651
|
+
// When maxPartBytes is configured, body is the PassThrough that
|
|
652
|
+
// wraps dicer's per-part Readable; otherwise body is dicer's
|
|
653
|
+
// per-part Readable directly. Both expose Node `Readable`.
|
|
654
|
+
body: publicBody
|
|
655
|
+
};
|
|
656
|
+
queue.push({ type: "part", part });
|
|
657
|
+
fireProgress();
|
|
658
|
+
};
|
|
659
|
+
partStream.once("header", onHeader);
|
|
660
|
+
partStream.on("error", (err) => {
|
|
661
|
+
if (cleaned) {
|
|
662
|
+
logger({
|
|
663
|
+
level: "warn",
|
|
664
|
+
msg: "multipart: late part-stream error after generator close",
|
|
665
|
+
meta: { errSummary: summarizeError(err) }
|
|
666
|
+
});
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
queue.signalError(err);
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
const onFinish = () => {
|
|
673
|
+
dicerFinished = true;
|
|
674
|
+
queue.signalEnd();
|
|
675
|
+
};
|
|
676
|
+
const onDicerError = (err) => {
|
|
677
|
+
if (cleaned) {
|
|
678
|
+
logger({
|
|
679
|
+
level: "warn",
|
|
680
|
+
msg: "multipart: late parser error after generator close",
|
|
681
|
+
meta: { errSummary: summarizeError(err) }
|
|
682
|
+
});
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
queue.signalError(err);
|
|
686
|
+
};
|
|
687
|
+
const onSourceData = (chunk) => {
|
|
688
|
+
bytesReceived += chunk.length;
|
|
689
|
+
timers.resetIdle();
|
|
690
|
+
};
|
|
691
|
+
const onSourceError = (err) => {
|
|
692
|
+
queue.signalError(err);
|
|
693
|
+
};
|
|
694
|
+
const onSourceEnd = () => {
|
|
695
|
+
setImmediate(() => {
|
|
696
|
+
if (dicerFinished) return;
|
|
697
|
+
if (cleaned) return;
|
|
698
|
+
queue.signalError(new MultipartTruncatedError(bytesReceived));
|
|
699
|
+
});
|
|
700
|
+
};
|
|
701
|
+
dicer.on("part", onPart);
|
|
702
|
+
dicer.on("finish", onFinish);
|
|
703
|
+
dicer.on("error", onDicerError);
|
|
704
|
+
source.on("data", onSourceData);
|
|
705
|
+
source.on("error", onSourceError);
|
|
706
|
+
source.on("end", onSourceEnd);
|
|
707
|
+
source.pipe(dicer);
|
|
708
|
+
const cleanup = () => {
|
|
709
|
+
if (cleaned) return;
|
|
710
|
+
cleaned = true;
|
|
711
|
+
source.off("data", onSourceData);
|
|
712
|
+
source.off("error", onSourceError);
|
|
713
|
+
source.off("end", onSourceEnd);
|
|
714
|
+
try {
|
|
715
|
+
source.unpipe(dicer);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger({
|
|
718
|
+
level: "warn",
|
|
719
|
+
msg: "multipart: unpipe failed during cleanup",
|
|
720
|
+
meta: { errSummary: summarizeError(err) }
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (!source.destroyed) {
|
|
724
|
+
source.destroy();
|
|
725
|
+
}
|
|
726
|
+
for (const part of queue.drainPendingParts()) {
|
|
727
|
+
part.body.destroy();
|
|
728
|
+
}
|
|
729
|
+
for (const partStream of allPartStreams) {
|
|
730
|
+
if (!partStream.destroyed) partStream.destroy();
|
|
731
|
+
}
|
|
732
|
+
allPartStreams.clear();
|
|
733
|
+
dicer.off("part", onPart);
|
|
734
|
+
dicer.off("finish", onFinish);
|
|
735
|
+
timers.signal.removeEventListener("abort", onCombinedAbort);
|
|
736
|
+
timers.cleanup();
|
|
737
|
+
};
|
|
738
|
+
try {
|
|
739
|
+
while (true) {
|
|
740
|
+
const item = await queue.next();
|
|
741
|
+
if (item.type === "end") return;
|
|
742
|
+
if (item.type === "error") throw item.err;
|
|
743
|
+
yield item.part;
|
|
744
|
+
}
|
|
745
|
+
} finally {
|
|
746
|
+
cleanup();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/fetch-and-handle-multipart.ts
|
|
751
|
+
async function fetchAndHandleMultipart(url, options) {
|
|
752
|
+
if (typeof options.parser !== "function") {
|
|
753
|
+
throw new TypeError("multipart: options.parser is required");
|
|
754
|
+
}
|
|
755
|
+
validatePositiveTimeout("idleTimeoutMs", options.idleTimeoutMs);
|
|
756
|
+
validatePositiveTimeout("totalTimeoutMs", options.totalTimeoutMs);
|
|
757
|
+
if (options.fetchInit !== void 0 && "signal" in options.fetchInit && options.fetchInit.signal !== void 0) {
|
|
758
|
+
throw new Error(
|
|
759
|
+
"multipart: pass signal via options.signal \u2014 fetchInit.signal is reserved for internal use"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
if (options.signal?.aborted === true) {
|
|
763
|
+
throw new MultipartAbortError(options.signal.reason);
|
|
764
|
+
}
|
|
765
|
+
const logger = options.logger ?? defaultLogger;
|
|
766
|
+
const startMs = Date.now();
|
|
767
|
+
let lastBytes = 0;
|
|
768
|
+
const onProgressForLayerA = (snap) => {
|
|
769
|
+
lastBytes = snap.bytes;
|
|
770
|
+
if (options.onProgress !== void 0) {
|
|
771
|
+
options.onProgress(snap);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
let res;
|
|
775
|
+
try {
|
|
776
|
+
const init = {
|
|
777
|
+
...options.fetchInit ?? {},
|
|
778
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
779
|
+
};
|
|
780
|
+
res = await fetch(url, init);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
const sig = options.signal;
|
|
783
|
+
if (sig?.aborted) {
|
|
784
|
+
throw new MultipartAbortError(sig.reason);
|
|
785
|
+
}
|
|
786
|
+
throw err;
|
|
787
|
+
}
|
|
788
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
789
|
+
if (!contentType.toLowerCase().startsWith("multipart/related")) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`multipart: response Content-Type is not multipart/related; got ${truncateForErrorEmbed(contentType)}`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const status = res.status;
|
|
795
|
+
const headers = res.headers;
|
|
796
|
+
const parseOpts = {
|
|
797
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
798
|
+
totalTimeoutMs: options.totalTimeoutMs,
|
|
799
|
+
onProgress: onProgressForLayerA,
|
|
800
|
+
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
801
|
+
...options.logger !== void 0 ? { logger: options.logger } : {},
|
|
802
|
+
...options.maxPartBytes !== void 0 ? { maxPartBytes: options.maxPartBytes } : {},
|
|
803
|
+
...options.maxParts !== void 0 ? { maxParts: options.maxParts } : {},
|
|
804
|
+
...options.maxHeadersPerPart !== void 0 ? { maxHeadersPerPart: options.maxHeadersPerPart } : {},
|
|
805
|
+
...options.maxHeaderBytesPerPart !== void 0 ? { maxHeaderBytesPerPart: options.maxHeaderBytesPerPart } : {}
|
|
806
|
+
};
|
|
807
|
+
const parts = [];
|
|
808
|
+
for await (const part of parseMultipartRelated(res, parseOpts)) {
|
|
809
|
+
const value = await invokeParser(options.parser, part);
|
|
810
|
+
if (value !== void 0) {
|
|
811
|
+
parts.push(value);
|
|
812
|
+
}
|
|
813
|
+
if (!part.body.destroyed && part.body.readable) {
|
|
814
|
+
for await (const _chunk of part.body) {
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const elapsedMs = Date.now() - startMs;
|
|
819
|
+
if (options.onProgress !== void 0) {
|
|
820
|
+
const rateBps = elapsedMs <= 0 ? 0 : Math.round(lastBytes * 1e3 / elapsedMs);
|
|
821
|
+
try {
|
|
822
|
+
options.onProgress({ bytes: lastBytes, elapsedMs, rateBps });
|
|
823
|
+
} catch (err) {
|
|
824
|
+
logger({
|
|
825
|
+
level: "warn",
|
|
826
|
+
msg: "multipart: onProgress threw on completion tick",
|
|
827
|
+
meta: { errSummary: summarizeError(err) }
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return { parts, bytes: lastBytes, elapsedMs, status, headers };
|
|
832
|
+
}
|
|
833
|
+
async function invokeParser(parser, part) {
|
|
834
|
+
return parser(part);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/stream-helpers.ts
|
|
838
|
+
function streamToString(readable, encoding = "utf8", options = {}) {
|
|
839
|
+
return new Promise((resolve, reject) => {
|
|
840
|
+
const chunks = [];
|
|
841
|
+
let total = 0;
|
|
842
|
+
const cap = options.maxBytes;
|
|
843
|
+
const onData = (chunk) => {
|
|
844
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk, encoding) : chunk;
|
|
845
|
+
total += buf.length;
|
|
846
|
+
if (cap !== void 0 && total > cap) {
|
|
847
|
+
cleanup();
|
|
848
|
+
readable.destroy();
|
|
849
|
+
reject(
|
|
850
|
+
new Error(`streamToString: input exceeded maxBytes (${String(cap)})`)
|
|
851
|
+
);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
chunks.push(buf);
|
|
855
|
+
};
|
|
856
|
+
const onError = (err) => {
|
|
857
|
+
cleanup();
|
|
858
|
+
reject(err);
|
|
859
|
+
};
|
|
860
|
+
const onEnd = () => {
|
|
861
|
+
cleanup();
|
|
862
|
+
resolve(Buffer.concat(chunks).toString(encoding));
|
|
863
|
+
};
|
|
864
|
+
const cleanup = () => {
|
|
865
|
+
readable.off("data", onData);
|
|
866
|
+
readable.off("error", onError);
|
|
867
|
+
readable.off("end", onEnd);
|
|
868
|
+
};
|
|
869
|
+
readable.on("data", onData);
|
|
870
|
+
readable.once("error", onError);
|
|
871
|
+
readable.once("end", onEnd);
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
function streamToBuffer(readable, options = {}) {
|
|
875
|
+
return new Promise((resolve, reject) => {
|
|
876
|
+
const chunks = [];
|
|
877
|
+
let total = 0;
|
|
878
|
+
const cap = options.maxBytes;
|
|
879
|
+
const onData = (chunk) => {
|
|
880
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
881
|
+
total += buf.length;
|
|
882
|
+
if (cap !== void 0 && total > cap) {
|
|
883
|
+
cleanup();
|
|
884
|
+
readable.destroy();
|
|
885
|
+
reject(
|
|
886
|
+
new Error(`streamToBuffer: input exceeded maxBytes (${String(cap)})`)
|
|
887
|
+
);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
chunks.push(buf);
|
|
891
|
+
};
|
|
892
|
+
const onError = (err) => {
|
|
893
|
+
cleanup();
|
|
894
|
+
reject(err);
|
|
895
|
+
};
|
|
896
|
+
const onEnd = () => {
|
|
897
|
+
cleanup();
|
|
898
|
+
resolve(chunks.length === 0 ? Buffer.alloc(0) : Buffer.concat(chunks));
|
|
899
|
+
};
|
|
900
|
+
const cleanup = () => {
|
|
901
|
+
readable.off("data", onData);
|
|
902
|
+
readable.off("error", onError);
|
|
903
|
+
readable.off("end", onEnd);
|
|
904
|
+
};
|
|
905
|
+
readable.on("data", onData);
|
|
906
|
+
readable.once("error", onError);
|
|
907
|
+
readable.once("end", onEnd);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export { MultipartAbortError, MultipartHeadersTooLargeError, MultipartIdleTimeoutError, MultipartPartTooLargeError, MultipartTooManyPartsError, MultipartTotalTimeoutError, MultipartTruncatedError, extractBoundary, fetchAndHandleMultipart, parseMultipartRelated, streamToBuffer, streamToString };
|
|
912
|
+
//# sourceMappingURL=index.js.map
|
|
913
|
+
//# sourceMappingURL=index.js.map
|