@ubercode/multipart-stream 1.0.0 → 1.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/README.md +2 -6
- package/dist/index.cjs +229 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -23
- package/dist/index.d.ts +22 -23
- package/dist/index.js +229 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,8 +14,7 @@ npm install @ubercode/multipart-stream
|
|
|
14
14
|
yarn add @ubercode/multipart-stream
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Requires Node `>= 20.18.0`.
|
|
18
|
-
exact).
|
|
17
|
+
Requires Node `>= 20.18.0`. The sole runtime dependency is `streamsearch@1.1.0`; multipart headers and part streams are parsed inside this package.
|
|
19
18
|
|
|
20
19
|
## Quickstart
|
|
21
20
|
|
|
@@ -101,10 +100,7 @@ for await (const part of parseMultipartRelated(req as unknown as Readable, {
|
|
|
101
100
|
}
|
|
102
101
|
```
|
|
103
102
|
|
|
104
|
-
The
|
|
105
|
-
your parser must drain or destroy each `part.body` before requesting the
|
|
106
|
-
next part. If you don't, the iterator's `finally` destroys leftover bodies
|
|
107
|
-
for you (FR-010), but that costs latency.
|
|
103
|
+
Drain or destroy each `part.body` before requesting the next part. The parser applies backpressure while a body is waiting for a reader; the iterator destroys leftover bodies when it closes.
|
|
108
104
|
|
|
109
105
|
## API
|
|
110
106
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var stream = require('stream');
|
|
4
|
-
var
|
|
4
|
+
var StreamSearch = require('streamsearch');
|
|
5
5
|
|
|
6
6
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var StreamSearch__default = /*#__PURE__*/_interopDefault(StreamSearch);
|
|
9
9
|
|
|
10
10
|
// src/errors.ts
|
|
11
11
|
var MultipartIdleTimeoutError = class extends Error {
|
|
@@ -299,7 +299,7 @@ function flattenHeaderValue(v) {
|
|
|
299
299
|
}
|
|
300
300
|
return "";
|
|
301
301
|
}
|
|
302
|
-
function
|
|
302
|
+
function flattenPartHeaders(raw) {
|
|
303
303
|
if (raw == null) return {};
|
|
304
304
|
const out = {};
|
|
305
305
|
for (const key of Object.keys(raw)) {
|
|
@@ -309,6 +309,213 @@ function flattenDicerHeaders(raw) {
|
|
|
309
309
|
}
|
|
310
310
|
return out;
|
|
311
311
|
}
|
|
312
|
+
var HEADER_END = Buffer.from("\r\n\r\n");
|
|
313
|
+
var INITIAL_CRLF = Buffer.from("\r\n");
|
|
314
|
+
var CLOSING_DASHES = Buffer.from("--");
|
|
315
|
+
var CLOSING_CR = Buffer.from("--\r");
|
|
316
|
+
var MAX_HEADER_BYTES = 80 * 1024;
|
|
317
|
+
var MAX_HEADER_PAIRS = 2e3;
|
|
318
|
+
var MultipartPartStream = class extends stream.Readable {
|
|
319
|
+
constructor(onRead) {
|
|
320
|
+
super();
|
|
321
|
+
this.onRead = onRead;
|
|
322
|
+
}
|
|
323
|
+
onRead;
|
|
324
|
+
_read() {
|
|
325
|
+
this.onRead();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
function parseHeaders(block) {
|
|
329
|
+
const headers = /* @__PURE__ */ Object.create(null);
|
|
330
|
+
if (block.length === 0) return headers;
|
|
331
|
+
const lines = block.toString("latin1").split("\r\n");
|
|
332
|
+
let previousName;
|
|
333
|
+
let count = 0;
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
if (line.startsWith(" ") || line.startsWith(" ")) {
|
|
336
|
+
if (previousName === void 0) {
|
|
337
|
+
throw new Error("Unexpected folded header value");
|
|
338
|
+
}
|
|
339
|
+
const values = headers[previousName];
|
|
340
|
+
if (values === void 0) throw new Error("Malformed part header");
|
|
341
|
+
values[values.length - 1] += line;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const colon = line.indexOf(":");
|
|
345
|
+
if (colon <= 0) throw new Error("Malformed part header");
|
|
346
|
+
const name = line.slice(0, colon);
|
|
347
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
348
|
+
throw new Error("Malformed part header");
|
|
349
|
+
}
|
|
350
|
+
const value = line.slice(colon + 1).replace(/^[ \t]/, "");
|
|
351
|
+
if (/[\x00-\x08\x0a-\x1f\x7f]/.test(value)) {
|
|
352
|
+
throw new Error("Malformed part header");
|
|
353
|
+
}
|
|
354
|
+
const key = name.toLowerCase();
|
|
355
|
+
(headers[key] ??= []).push(value);
|
|
356
|
+
previousName = key;
|
|
357
|
+
if (++count > MAX_HEADER_PAIRS) throw new Error("Too many part headers");
|
|
358
|
+
}
|
|
359
|
+
return headers;
|
|
360
|
+
}
|
|
361
|
+
var MultipartParser = class extends stream.Writable {
|
|
362
|
+
needle;
|
|
363
|
+
search;
|
|
364
|
+
state = "preamble";
|
|
365
|
+
part;
|
|
366
|
+
headerBytes = Buffer.alloc(0);
|
|
367
|
+
suffix = Buffer.alloc(0);
|
|
368
|
+
candidate = false;
|
|
369
|
+
pausedPart;
|
|
370
|
+
pendingWrite;
|
|
371
|
+
openParts = 0;
|
|
372
|
+
finalCallback;
|
|
373
|
+
constructor(boundary) {
|
|
374
|
+
super();
|
|
375
|
+
if (boundary.length === 0) throw new TypeError("Boundary required");
|
|
376
|
+
this.needle = Buffer.from(`\r
|
|
377
|
+
--${boundary}`);
|
|
378
|
+
this.search = new StreamSearch__default.default(this.needle, (matched, data, start, end, safe) => {
|
|
379
|
+
if (this.state !== "done" && data && start < end) {
|
|
380
|
+
const chunk = data.subarray(start, end);
|
|
381
|
+
this.consume(safe ? chunk : Buffer.from(chunk));
|
|
382
|
+
}
|
|
383
|
+
if (this.state === "done") return;
|
|
384
|
+
if (matched) {
|
|
385
|
+
if (this.state === "headers") throw new Error("Malformed part header");
|
|
386
|
+
this.candidate = true;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
this.search.push(INITIAL_CRLF);
|
|
390
|
+
}
|
|
391
|
+
_write(chunk, _encoding, callback) {
|
|
392
|
+
try {
|
|
393
|
+
this.search.push(chunk);
|
|
394
|
+
if (this.pausedPart) this.pendingWrite = callback;
|
|
395
|
+
else callback();
|
|
396
|
+
} catch (error) {
|
|
397
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
_final(callback) {
|
|
401
|
+
try {
|
|
402
|
+
this.search.destroy();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (this.candidate && this.suffix.equals(CLOSING_DASHES)) {
|
|
408
|
+
this.candidate = false;
|
|
409
|
+
this.endPart();
|
|
410
|
+
this.state = "done";
|
|
411
|
+
}
|
|
412
|
+
if (this.state !== "done") {
|
|
413
|
+
const error = new Error("Unexpected end of multipart data");
|
|
414
|
+
this.endPart();
|
|
415
|
+
callback(error);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (this.openParts === 0) callback();
|
|
419
|
+
else this.finalCallback = callback;
|
|
420
|
+
}
|
|
421
|
+
resumeWrite(part) {
|
|
422
|
+
if (this.pausedPart !== part) return;
|
|
423
|
+
this.pausedPart = void 0;
|
|
424
|
+
const callback = this.pendingWrite;
|
|
425
|
+
this.pendingWrite = void 0;
|
|
426
|
+
callback?.();
|
|
427
|
+
}
|
|
428
|
+
endPart() {
|
|
429
|
+
const part = this.part;
|
|
430
|
+
if (part === void 0) return;
|
|
431
|
+
this.part = void 0;
|
|
432
|
+
part.push(null);
|
|
433
|
+
}
|
|
434
|
+
beginPart() {
|
|
435
|
+
this.headerBytes = Buffer.alloc(0);
|
|
436
|
+
this.state = "headers";
|
|
437
|
+
const part = new MultipartPartStream(() => this.resumeWrite(part));
|
|
438
|
+
this.part = part;
|
|
439
|
+
this.openParts++;
|
|
440
|
+
let settled = false;
|
|
441
|
+
const onPartDone = () => {
|
|
442
|
+
if (settled) return;
|
|
443
|
+
settled = true;
|
|
444
|
+
this.openParts--;
|
|
445
|
+
this.resumeWrite(part);
|
|
446
|
+
if (this.openParts === 0 && this.finalCallback) {
|
|
447
|
+
const callback = this.finalCallback;
|
|
448
|
+
this.finalCallback = void 0;
|
|
449
|
+
callback();
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
part.once("end", onPartDone);
|
|
453
|
+
part.once("close", onPartDone);
|
|
454
|
+
this.emit("part", part);
|
|
455
|
+
}
|
|
456
|
+
consume(data) {
|
|
457
|
+
if (this.candidate) {
|
|
458
|
+
if (this.suffix.length < 2) {
|
|
459
|
+
const taken = Math.min(2 - this.suffix.length, data.length);
|
|
460
|
+
this.suffix = Buffer.concat([this.suffix, data.subarray(0, taken)]);
|
|
461
|
+
data = data.subarray(taken);
|
|
462
|
+
}
|
|
463
|
+
if (this.suffix.length < 2) return;
|
|
464
|
+
if (this.suffix.equals(CLOSING_DASHES)) {
|
|
465
|
+
if (data.length === 0) return;
|
|
466
|
+
if (data[0] === 13) {
|
|
467
|
+
this.suffix = CLOSING_CR;
|
|
468
|
+
data = data.subarray(1);
|
|
469
|
+
if (data.length === 0) return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (this.suffix.equals(CLOSING_CR) && data.length === 0) return;
|
|
473
|
+
const suffix = this.suffix;
|
|
474
|
+
this.suffix = Buffer.alloc(0);
|
|
475
|
+
this.candidate = false;
|
|
476
|
+
if (suffix.equals(INITIAL_CRLF)) {
|
|
477
|
+
this.endPart();
|
|
478
|
+
this.beginPart();
|
|
479
|
+
} else if (suffix.equals(CLOSING_CR) && data[0] === 10) {
|
|
480
|
+
data = data.subarray(1);
|
|
481
|
+
this.endPart();
|
|
482
|
+
this.state = "done";
|
|
483
|
+
return;
|
|
484
|
+
} else if (this.state === "headers") {
|
|
485
|
+
data = Buffer.concat([this.needle, suffix, data]);
|
|
486
|
+
} else if (this.state === "body") {
|
|
487
|
+
this.consumeBody(this.needle);
|
|
488
|
+
this.consumeBody(suffix);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (this.state === "headers") {
|
|
492
|
+
const pending = this.headerBytes.length;
|
|
493
|
+
const scan = data.subarray(0, MAX_HEADER_BYTES + HEADER_END.length - pending);
|
|
494
|
+
const combined = Buffer.concat([this.headerBytes, scan]);
|
|
495
|
+
const empty = combined.length >= 2 && combined.subarray(0, 2).equals(INITIAL_CRLF);
|
|
496
|
+
const end = empty ? 0 : combined.indexOf(HEADER_END);
|
|
497
|
+
if (end < 0) {
|
|
498
|
+
if (combined.length > MAX_HEADER_BYTES) throw new Error("Part headers too large");
|
|
499
|
+
this.headerBytes = combined;
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (end > MAX_HEADER_BYTES) throw new Error("Part headers too large");
|
|
503
|
+
const headers = parseHeaders(combined.subarray(0, end));
|
|
504
|
+
const rawHeaders = Buffer.from(combined.subarray(0, end + (empty ? 2 : HEADER_END.length)));
|
|
505
|
+
this.headerBytes = Buffer.alloc(0);
|
|
506
|
+
this.state = "body";
|
|
507
|
+
this.part?.emit("header", headers, rawHeaders);
|
|
508
|
+
this.consumeBody(data.subarray(end + (empty ? 2 : HEADER_END.length) - pending));
|
|
509
|
+
} else if (this.state === "body") {
|
|
510
|
+
this.consumeBody(data);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
consumeBody(data) {
|
|
514
|
+
if (data.length > 0 && this.part && !this.part.destroyed && !this.part.push(data)) {
|
|
515
|
+
this.pausedPart = this.part;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
312
519
|
function looksLikeResponse(input) {
|
|
313
520
|
if (typeof Response !== "undefined" && input instanceof Response) {
|
|
314
521
|
return true;
|
|
@@ -470,19 +677,6 @@ function setupTimers(opts, startMs) {
|
|
|
470
677
|
}
|
|
471
678
|
|
|
472
679
|
// src/parse-multipart-related.ts
|
|
473
|
-
function measureHeaderValueBytes(value) {
|
|
474
|
-
if (typeof value === "string") return Buffer.byteLength(value);
|
|
475
|
-
if (Array.isArray(value)) {
|
|
476
|
-
let total = 0;
|
|
477
|
-
for (const inner of value) {
|
|
478
|
-
total += measureHeaderValueBytes(inner);
|
|
479
|
-
}
|
|
480
|
-
return total;
|
|
481
|
-
}
|
|
482
|
-
if (Buffer.isBuffer(value)) return value.length;
|
|
483
|
-
return 0;
|
|
484
|
-
}
|
|
485
|
-
var Dicer = dicerMod__default.default.default ?? dicerMod__default.default;
|
|
486
680
|
function parseMultipartRelated(input, opts) {
|
|
487
681
|
return parseMultipartRelatedImpl(input, opts);
|
|
488
682
|
}
|
|
@@ -497,11 +691,11 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
497
691
|
boundary: opts.boundary
|
|
498
692
|
});
|
|
499
693
|
const logger = opts.logger ?? defaultLogger;
|
|
500
|
-
const
|
|
694
|
+
const parser = new MultipartParser(boundary);
|
|
501
695
|
const queue = createQueueNotifier();
|
|
502
696
|
let bytesReceived = 0;
|
|
503
697
|
let nextPartIndex = 0;
|
|
504
|
-
let
|
|
698
|
+
let parserFinished = false;
|
|
505
699
|
let cleaned = false;
|
|
506
700
|
let abortPushed = false;
|
|
507
701
|
const allPartStreams = /* @__PURE__ */ new Set();
|
|
@@ -559,21 +753,13 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
559
753
|
const headersAccumulator = {
|
|
560
754
|
value: {}
|
|
561
755
|
};
|
|
562
|
-
const onHeader = (raw) => {
|
|
756
|
+
const onHeader = (raw, rawHeaderBlock) => {
|
|
563
757
|
const bag = raw;
|
|
564
758
|
let headerCount = 0;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
for (const name of Object.keys(bag)) {
|
|
568
|
-
const value = bag[name];
|
|
569
|
-
const nameBytes = Buffer.byteLength(name);
|
|
570
|
-
const values = Array.isArray(value) ? value : [value];
|
|
571
|
-
for (const inner of values) {
|
|
572
|
-
headerCount += 1;
|
|
573
|
-
headerBytes += nameBytes + 4 + measureHeaderValueBytes(inner);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
759
|
+
if (bag !== void 0) {
|
|
760
|
+
for (const values of Object.values(bag)) headerCount += values.length;
|
|
576
761
|
}
|
|
762
|
+
const headerBytes = rawHeaderBlock.length;
|
|
577
763
|
if (headerCount > maxHeadersPerPart) {
|
|
578
764
|
if (!partStream.destroyed) partStream.destroy();
|
|
579
765
|
queue.signalError(
|
|
@@ -598,7 +784,7 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
598
784
|
);
|
|
599
785
|
return;
|
|
600
786
|
}
|
|
601
|
-
headersAccumulator.value =
|
|
787
|
+
headersAccumulator.value = flattenPartHeaders(
|
|
602
788
|
raw
|
|
603
789
|
);
|
|
604
790
|
const headers = headersAccumulator.value;
|
|
@@ -650,12 +836,12 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
650
836
|
index: partIndex,
|
|
651
837
|
boundary,
|
|
652
838
|
headers,
|
|
653
|
-
rawHeaders:
|
|
839
|
+
rawHeaders: rawHeaderBlock,
|
|
654
840
|
contentType,
|
|
655
841
|
...contentId !== void 0 ? { contentId } : {},
|
|
656
842
|
...contentLength !== void 0 ? { contentLength } : {},
|
|
657
843
|
// When maxPartBytes is configured, body is the PassThrough that
|
|
658
|
-
// wraps
|
|
844
|
+
// wraps parser's per-part Readable; otherwise body is parser's
|
|
659
845
|
// per-part Readable directly. Both expose Node `Readable`.
|
|
660
846
|
body: publicBody
|
|
661
847
|
};
|
|
@@ -676,10 +862,10 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
676
862
|
});
|
|
677
863
|
};
|
|
678
864
|
const onFinish = () => {
|
|
679
|
-
|
|
865
|
+
parserFinished = true;
|
|
680
866
|
queue.signalEnd();
|
|
681
867
|
};
|
|
682
|
-
const
|
|
868
|
+
const onParserError = (err) => {
|
|
683
869
|
if (cleaned) {
|
|
684
870
|
logger({
|
|
685
871
|
level: "warn",
|
|
@@ -699,18 +885,18 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
699
885
|
};
|
|
700
886
|
const onSourceEnd = () => {
|
|
701
887
|
setImmediate(() => {
|
|
702
|
-
if (
|
|
888
|
+
if (parserFinished) return;
|
|
703
889
|
if (cleaned) return;
|
|
704
890
|
queue.signalError(new MultipartTruncatedError(bytesReceived));
|
|
705
891
|
});
|
|
706
892
|
};
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
893
|
+
parser.on("part", onPart);
|
|
894
|
+
parser.on("finish", onFinish);
|
|
895
|
+
parser.on("error", onParserError);
|
|
710
896
|
source.on("data", onSourceData);
|
|
711
897
|
source.on("error", onSourceError);
|
|
712
898
|
source.on("end", onSourceEnd);
|
|
713
|
-
source.pipe(
|
|
899
|
+
source.pipe(parser);
|
|
714
900
|
const cleanup = () => {
|
|
715
901
|
if (cleaned) return;
|
|
716
902
|
cleaned = true;
|
|
@@ -718,7 +904,7 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
718
904
|
source.off("error", onSourceError);
|
|
719
905
|
source.off("end", onSourceEnd);
|
|
720
906
|
try {
|
|
721
|
-
source.unpipe(
|
|
907
|
+
source.unpipe(parser);
|
|
722
908
|
} catch (err) {
|
|
723
909
|
logger({
|
|
724
910
|
level: "warn",
|
|
@@ -736,8 +922,8 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
736
922
|
if (!partStream.destroyed) partStream.destroy();
|
|
737
923
|
}
|
|
738
924
|
allPartStreams.clear();
|
|
739
|
-
|
|
740
|
-
|
|
925
|
+
parser.off("part", onPart);
|
|
926
|
+
parser.off("finish", onFinish);
|
|
741
927
|
timers.signal.removeEventListener("abort", onCombinedAbort);
|
|
742
928
|
timers.cleanup();
|
|
743
929
|
};
|