@remix-run/multipart-parser 0.13.0 → 0.14.1
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 +8 -8
- package/dist/lib/buffer-search.d.ts.map +1 -1
- package/dist/lib/buffer-search.js +4 -3
- package/dist/lib/multipart-request.d.ts +3 -3
- package/dist/lib/multipart-request.js +3 -3
- package/dist/lib/multipart.d.ts +28 -8
- package/dist/lib/multipart.d.ts.map +1 -1
- package/dist/lib/multipart.js +26 -11
- package/dist/lib/multipart.node.d.ts +4 -4
- package/dist/lib/multipart.node.js +4 -4
- package/package.json +7 -7
- package/src/lib/buffer-search.ts +6 -3
- package/src/lib/multipart-request.ts +3 -3
- package/src/lib/multipart.node.ts +4 -4
- package/src/lib/multipart.ts +40 -15
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`multipart-parser` is a fast, streaming multipart parser that works in **any JavaScript environment**, from serverless functions to traditional servers. Whether you're handling file uploads, parsing email attachments, or working with multipart API responses, `multipart-parser` has you covered.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why multipart-parser?
|
|
6
6
|
|
|
7
7
|
- **Universal JavaScript** - One library that works everywhere: Node.js, Bun, Deno, Cloudflare Workers, and browsers
|
|
8
8
|
- **Blazing Fast** - Consistently outperforms popular alternatives like busboy in benchmarks
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
- **Standards Based** - Built on the web standard [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) for maximum compatibility
|
|
13
13
|
- **Production Ready** - Battle-tested error handling with specific error types for common scenarios
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Features
|
|
16
16
|
|
|
17
|
-
- Parse file uploads (`multipart/form-data`) with automatic field and file detection
|
|
18
|
-
- Support for all `multipart/*` content types (mixed, alternative, related, etc.)
|
|
19
|
-
- Convenient `MultipartPart` API with `arrayBuffer`, `bytes`, `text`, `size`, and metadata access
|
|
20
|
-
- Built-in file size limiting to prevent abuse
|
|
21
|
-
- First-class Node.js support with native `http.IncomingMessage` compatibility
|
|
22
|
-
- [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
|
|
17
|
+
- **File Upload Parsing** - Parse file uploads (`multipart/form-data`) with automatic field and file detection
|
|
18
|
+
- **Full Multipart Support** - Support for all `multipart/*` content types (mixed, alternative, related, etc.)
|
|
19
|
+
- **Convenient API** - `MultipartPart` API with `arrayBuffer`, `bytes`, `text`, `size`, and metadata access
|
|
20
|
+
- **File Size Limiting** - Built-in file size limiting to prevent abuse
|
|
21
|
+
- **Node.js Support** - First-class Node.js support with native `http.IncomingMessage` compatibility
|
|
22
|
+
- **Runtime Demos** - [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffer-search.d.ts","sourceRoot":"","sources":["../../src/lib/buffer-search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC/C;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"buffer-search.d.ts","sourceRoot":"","sources":["../../src/lib/buffer-search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC/C;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAkC5D;AAED,MAAM,WAAW,yBAAyB;IACxC,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAA;CAC/B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,yBAAyB,CAyBlF"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export function createSearch(pattern) {
|
|
2
2
|
let needle = new TextEncoder().encode(pattern);
|
|
3
3
|
let search;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Use the built-in Buffer.indexOf method on Node.js for better perf.
|
|
5
|
+
let BufferClass = globalThis.Buffer;
|
|
6
|
+
if (BufferClass && !('Bun' in globalThis || 'Deno' in globalThis)) {
|
|
7
|
+
search = (haystack, start = 0) => BufferClass.prototype.indexOf.call(haystack, needle, start);
|
|
7
8
|
}
|
|
8
9
|
else {
|
|
9
10
|
let needleEnd = needle.length - 1;
|
|
@@ -3,14 +3,14 @@ import type { MultipartParserOptions, MultipartPart } from './multipart.ts';
|
|
|
3
3
|
* Extracts the boundary string from a `multipart/*` content type.
|
|
4
4
|
*
|
|
5
5
|
* @param contentType The `Content-Type` header value from the request
|
|
6
|
-
* @
|
|
6
|
+
* @returns The boundary string if found, or null if not present
|
|
7
7
|
*/
|
|
8
8
|
export declare function getMultipartBoundary(contentType: string): string | null;
|
|
9
9
|
/**
|
|
10
10
|
* Returns true if the given request contains multipart data.
|
|
11
11
|
*
|
|
12
12
|
* @param request The `Request` object to check
|
|
13
|
-
* @
|
|
13
|
+
* @returns `true` if the request is a multipart request, `false` otherwise
|
|
14
14
|
*/
|
|
15
15
|
export declare function isMultipartRequest(request: Request): boolean;
|
|
16
16
|
/**
|
|
@@ -19,7 +19,7 @@ export declare function isMultipartRequest(request: Request): boolean;
|
|
|
19
19
|
*
|
|
20
20
|
* @param request The `Request` object containing multipart data
|
|
21
21
|
* @param options Optional parser options, such as `maxHeaderSize` and `maxFileSize`
|
|
22
|
-
* @
|
|
22
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
23
23
|
*/
|
|
24
24
|
export declare function parseMultipartRequest(request: Request, options?: MultipartParserOptions): AsyncGenerator<MultipartPart, void, unknown>;
|
|
25
25
|
//# sourceMappingURL=multipart-request.d.ts.map
|
|
@@ -3,7 +3,7 @@ import { MultipartParseError, parseMultipartStream } from "./multipart.js";
|
|
|
3
3
|
* Extracts the boundary string from a `multipart/*` content type.
|
|
4
4
|
*
|
|
5
5
|
* @param contentType The `Content-Type` header value from the request
|
|
6
|
-
* @
|
|
6
|
+
* @returns The boundary string if found, or null if not present
|
|
7
7
|
*/
|
|
8
8
|
export function getMultipartBoundary(contentType) {
|
|
9
9
|
let match = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType);
|
|
@@ -13,7 +13,7 @@ export function getMultipartBoundary(contentType) {
|
|
|
13
13
|
* Returns true if the given request contains multipart data.
|
|
14
14
|
*
|
|
15
15
|
* @param request The `Request` object to check
|
|
16
|
-
* @
|
|
16
|
+
* @returns `true` if the request is a multipart request, `false` otherwise
|
|
17
17
|
*/
|
|
18
18
|
export function isMultipartRequest(request) {
|
|
19
19
|
let contentType = request.headers.get('Content-Type');
|
|
@@ -25,7 +25,7 @@ export function isMultipartRequest(request) {
|
|
|
25
25
|
*
|
|
26
26
|
* @param request The `Request` object containing multipart data
|
|
27
27
|
* @param options Optional parser options, such as `maxHeaderSize` and `maxFileSize`
|
|
28
|
-
* @
|
|
28
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
29
29
|
*/
|
|
30
30
|
export async function* parseMultipartRequest(request, options) {
|
|
31
31
|
if (!isMultipartRequest(request)) {
|
package/dist/lib/multipart.d.ts
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import Headers from '@remix-run/headers';
|
|
2
1
|
/**
|
|
3
2
|
* The base class for errors thrown by the multipart parser.
|
|
4
3
|
*/
|
|
5
4
|
export declare class MultipartParseError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* @param message The error message
|
|
7
|
+
*/
|
|
6
8
|
constructor(message: string);
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* An error thrown when the maximum allowed size of a header is exceeded.
|
|
10
12
|
*/
|
|
11
13
|
export declare class MaxHeaderSizeExceededError extends MultipartParseError {
|
|
14
|
+
/**
|
|
15
|
+
* @param maxHeaderSize The maximum header size that was exceeded
|
|
16
|
+
*/
|
|
12
17
|
constructor(maxHeaderSize: number);
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
20
|
* An error thrown when the maximum allowed size of a file is exceeded.
|
|
16
21
|
*/
|
|
17
22
|
export declare class MaxFileSizeExceededError extends MultipartParseError {
|
|
23
|
+
/**
|
|
24
|
+
* @param maxFileSize The maximum file size that was exceeded
|
|
25
|
+
*/
|
|
18
26
|
constructor(maxFileSize: number);
|
|
19
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Options for parsing a multipart message.
|
|
30
|
+
*/
|
|
20
31
|
export interface ParseMultipartOptions {
|
|
21
32
|
/**
|
|
22
33
|
* The boundary string used to separate parts in the multipart message,
|
|
@@ -27,14 +38,14 @@ export interface ParseMultipartOptions {
|
|
|
27
38
|
* The maximum allowed size of a header in bytes. If an individual part's header
|
|
28
39
|
* exceeds this size, a `MaxHeaderSizeExceededError` will be thrown.
|
|
29
40
|
*
|
|
30
|
-
*
|
|
41
|
+
* @default 8192 (8 KiB)
|
|
31
42
|
*/
|
|
32
43
|
maxHeaderSize?: number;
|
|
33
44
|
/**
|
|
34
45
|
* The maximum allowed size of a file in bytes. If an individual part's content
|
|
35
46
|
* exceeds this size, a `MaxFileSizeExceededError` will be thrown.
|
|
36
47
|
*
|
|
37
|
-
*
|
|
48
|
+
* @default 2097152 (2 MiB)
|
|
38
49
|
*/
|
|
39
50
|
maxFileSize?: number;
|
|
40
51
|
}
|
|
@@ -46,7 +57,7 @@ export interface ParseMultipartOptions {
|
|
|
46
57
|
*
|
|
47
58
|
* @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks
|
|
48
59
|
* @param options Options for the parser
|
|
49
|
-
* @
|
|
60
|
+
* @returns A generator that yields `MultipartPart` objects
|
|
50
61
|
*/
|
|
51
62
|
export declare function parseMultipart(message: Uint8Array | Iterable<Uint8Array>, options: ParseMultipartOptions): Generator<MultipartPart, void, unknown>;
|
|
52
63
|
/**
|
|
@@ -57,9 +68,12 @@ export declare function parseMultipart(message: Uint8Array | Iterable<Uint8Array
|
|
|
57
68
|
*
|
|
58
69
|
* @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`
|
|
59
70
|
* @param options Options for the parser
|
|
60
|
-
* @
|
|
71
|
+
* @returns An async generator that yields `MultipartPart` objects
|
|
61
72
|
*/
|
|
62
73
|
export declare function parseMultipartStream(stream: ReadableStream<Uint8Array>, options: ParseMultipartOptions): AsyncGenerator<MultipartPart, void, unknown>;
|
|
74
|
+
/**
|
|
75
|
+
* Options for configuring a `MultipartParser`.
|
|
76
|
+
*/
|
|
63
77
|
export type MultipartParserOptions = Omit<ParseMultipartOptions, 'boundary'>;
|
|
64
78
|
/**
|
|
65
79
|
* A streaming parser for `multipart/*` HTTP messages.
|
|
@@ -69,12 +83,16 @@ export declare class MultipartParser {
|
|
|
69
83
|
readonly boundary: string;
|
|
70
84
|
readonly maxHeaderSize: number;
|
|
71
85
|
readonly maxFileSize: number;
|
|
86
|
+
/**
|
|
87
|
+
* @param boundary The boundary string used to separate parts
|
|
88
|
+
* @param options Options for the parser
|
|
89
|
+
*/
|
|
72
90
|
constructor(boundary: string, options?: MultipartParserOptions);
|
|
73
91
|
/**
|
|
74
92
|
* Write a chunk of data to the parser.
|
|
75
93
|
*
|
|
76
94
|
* @param chunk A chunk of data to write to the parser
|
|
77
|
-
* @
|
|
95
|
+
* @returns A generator yielding `MultipartPart` objects as they are parsed
|
|
78
96
|
*/
|
|
79
97
|
write(chunk: Uint8Array): Generator<MultipartPart, void, unknown>;
|
|
80
98
|
/**
|
|
@@ -82,8 +100,6 @@ export declare class MultipartParser {
|
|
|
82
100
|
*
|
|
83
101
|
* Note: This will throw if the multipart message is incomplete or
|
|
84
102
|
* wasn't properly terminated.
|
|
85
|
-
*
|
|
86
|
-
* @return void
|
|
87
103
|
*/
|
|
88
104
|
finish(): void;
|
|
89
105
|
}
|
|
@@ -96,6 +112,10 @@ export declare class MultipartPart {
|
|
|
96
112
|
* The raw content of this part as an array of `Uint8Array` chunks.
|
|
97
113
|
*/
|
|
98
114
|
readonly content: Uint8Array[];
|
|
115
|
+
/**
|
|
116
|
+
* @param header The raw header bytes
|
|
117
|
+
* @param content The content chunks
|
|
118
|
+
*/
|
|
99
119
|
constructor(header: Uint8Array, content: Uint8Array[]);
|
|
100
120
|
/**
|
|
101
121
|
* The content of this part as an `ArrayBuffer`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C;;OAEG;IACH,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;IACjE;;OAEG;IACH,YAAY,aAAa,EAAE,MAAM,EAGhC;CACF;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB;IAC/D;;OAEG;IACH,YAAY,WAAW,EAAE,MAAM,EAG9B;CACF;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC,EAC1C,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAmBzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,CAAA;AAa5E;;GAEG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAa5B;;;OAGG;IACH,YAAY,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,EAU7D;IAED;;;;;OAKG;IACF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CA+GjE;IAWD;;;;;OAKG;IACH,MAAM,IAAI,IAAI,CAIb;CACF;AAID;;GAEG;AACH,qBAAa,aAAa;;IACxB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAA;IAK9B;;;OAGG;IACH,YAAY,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,EAGpD;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED;;;OAGG;IACH,IAAI,KAAK,IAAI,UAAU,CAUtB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAMrB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAQjB;IAED;;;;;OAKG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
package/dist/lib/multipart.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ContentDisposition, ContentType, parse as parseRawHeaders } from '@remix-run/headers';
|
|
2
|
+
import { createSearch, createPartialTailSearch, } from "./buffer-search.js";
|
|
2
3
|
import { readStream } from "./read-stream.js";
|
|
3
|
-
import { createSearch, createPartialTailSearch } from "./buffer-search.js";
|
|
4
4
|
/**
|
|
5
5
|
* The base class for errors thrown by the multipart parser.
|
|
6
6
|
*/
|
|
7
7
|
export class MultipartParseError extends Error {
|
|
8
|
+
/**
|
|
9
|
+
* @param message The error message
|
|
10
|
+
*/
|
|
8
11
|
constructor(message) {
|
|
9
12
|
super(message);
|
|
10
13
|
this.name = 'MultipartParseError';
|
|
@@ -14,6 +17,9 @@ export class MultipartParseError extends Error {
|
|
|
14
17
|
* An error thrown when the maximum allowed size of a header is exceeded.
|
|
15
18
|
*/
|
|
16
19
|
export class MaxHeaderSizeExceededError extends MultipartParseError {
|
|
20
|
+
/**
|
|
21
|
+
* @param maxHeaderSize The maximum header size that was exceeded
|
|
22
|
+
*/
|
|
17
23
|
constructor(maxHeaderSize) {
|
|
18
24
|
super(`Multipart header size exceeds maximum allowed size of ${maxHeaderSize} bytes`);
|
|
19
25
|
this.name = 'MaxHeaderSizeExceededError';
|
|
@@ -23,6 +29,9 @@ export class MaxHeaderSizeExceededError extends MultipartParseError {
|
|
|
23
29
|
* An error thrown when the maximum allowed size of a file is exceeded.
|
|
24
30
|
*/
|
|
25
31
|
export class MaxFileSizeExceededError extends MultipartParseError {
|
|
32
|
+
/**
|
|
33
|
+
* @param maxFileSize The maximum file size that was exceeded
|
|
34
|
+
*/
|
|
26
35
|
constructor(maxFileSize) {
|
|
27
36
|
super(`File size exceeds maximum allowed size of ${maxFileSize} bytes`);
|
|
28
37
|
this.name = 'MaxFileSizeExceededError';
|
|
@@ -36,7 +45,7 @@ export class MaxFileSizeExceededError extends MultipartParseError {
|
|
|
36
45
|
*
|
|
37
46
|
* @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks
|
|
38
47
|
* @param options Options for the parser
|
|
39
|
-
* @
|
|
48
|
+
* @returns A generator that yields `MultipartPart` objects
|
|
40
49
|
*/
|
|
41
50
|
export function* parseMultipart(message, options) {
|
|
42
51
|
let parser = new MultipartParser(options.boundary, {
|
|
@@ -64,7 +73,7 @@ export function* parseMultipart(message, options) {
|
|
|
64
73
|
*
|
|
65
74
|
* @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`
|
|
66
75
|
* @param options Options for the parser
|
|
67
|
-
* @
|
|
76
|
+
* @returns An async generator that yields `MultipartPart` objects
|
|
68
77
|
*/
|
|
69
78
|
export async function* parseMultipartStream(stream, options) {
|
|
70
79
|
let parser = new MultipartParser(options.boundary, {
|
|
@@ -103,6 +112,10 @@ export class MultipartParser {
|
|
|
103
112
|
#buffer = null;
|
|
104
113
|
#currentPart = null;
|
|
105
114
|
#contentLength = 0;
|
|
115
|
+
/**
|
|
116
|
+
* @param boundary The boundary string used to separate parts
|
|
117
|
+
* @param options Options for the parser
|
|
118
|
+
*/
|
|
106
119
|
constructor(boundary, options) {
|
|
107
120
|
this.boundary = boundary;
|
|
108
121
|
this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb;
|
|
@@ -117,7 +130,7 @@ export class MultipartParser {
|
|
|
117
130
|
* Write a chunk of data to the parser.
|
|
118
131
|
*
|
|
119
132
|
* @param chunk A chunk of data to write to the parser
|
|
120
|
-
* @
|
|
133
|
+
* @returns A generator yielding `MultipartPart` objects as they are parsed
|
|
121
134
|
*/
|
|
122
135
|
*write(chunk) {
|
|
123
136
|
if (this.#state === MultipartParserStateDone) {
|
|
@@ -216,8 +229,6 @@ export class MultipartParser {
|
|
|
216
229
|
*
|
|
217
230
|
* Note: This will throw if the multipart message is incomplete or
|
|
218
231
|
* wasn't properly terminated.
|
|
219
|
-
*
|
|
220
|
-
* @return void
|
|
221
232
|
*/
|
|
222
233
|
finish() {
|
|
223
234
|
if (this.#state !== MultipartParserStateDone) {
|
|
@@ -236,6 +247,10 @@ export class MultipartPart {
|
|
|
236
247
|
content;
|
|
237
248
|
#header;
|
|
238
249
|
#headers;
|
|
250
|
+
/**
|
|
251
|
+
* @param header The raw header bytes
|
|
252
|
+
* @param content The content chunks
|
|
253
|
+
*/
|
|
239
254
|
constructor(header, content) {
|
|
240
255
|
this.#header = header;
|
|
241
256
|
this.content = content;
|
|
@@ -264,7 +279,7 @@ export class MultipartPart {
|
|
|
264
279
|
*/
|
|
265
280
|
get headers() {
|
|
266
281
|
if (!this.#headers) {
|
|
267
|
-
this.#headers =
|
|
282
|
+
this.#headers = parseRawHeaders(decoder.decode(this.#header));
|
|
268
283
|
}
|
|
269
284
|
return this.#headers;
|
|
270
285
|
}
|
|
@@ -284,19 +299,19 @@ export class MultipartPart {
|
|
|
284
299
|
* The filename of the part, if it is a file upload.
|
|
285
300
|
*/
|
|
286
301
|
get filename() {
|
|
287
|
-
return this.headers.
|
|
302
|
+
return ContentDisposition.from(this.headers.get('content-disposition')).preferredFilename;
|
|
288
303
|
}
|
|
289
304
|
/**
|
|
290
305
|
* The media type of the part.
|
|
291
306
|
*/
|
|
292
307
|
get mediaType() {
|
|
293
|
-
return this.headers.
|
|
308
|
+
return ContentType.from(this.headers.get('content-type')).mediaType;
|
|
294
309
|
}
|
|
295
310
|
/**
|
|
296
311
|
* The name of the part, usually the `name` of the field in the `<form>` that submitted the request.
|
|
297
312
|
*/
|
|
298
313
|
get name() {
|
|
299
|
-
return this.headers.
|
|
314
|
+
return ContentDisposition.from(this.headers.get('content-disposition')).name;
|
|
300
315
|
}
|
|
301
316
|
/**
|
|
302
317
|
* The size of the content in bytes.
|
|
@@ -9,7 +9,7 @@ import type { ParseMultipartOptions, MultipartParserOptions, MultipartPart } fro
|
|
|
9
9
|
*
|
|
10
10
|
* @param message The multipart message as a `Buffer` or an iterable of `Buffer` chunks
|
|
11
11
|
* @param options Options for the parser
|
|
12
|
-
* @
|
|
12
|
+
* @returns A generator yielding `MultipartPart` objects
|
|
13
13
|
*/
|
|
14
14
|
export declare function parseMultipart(message: Buffer | Iterable<Buffer>, options: ParseMultipartOptions): Generator<MultipartPart, void, unknown>;
|
|
15
15
|
/**
|
|
@@ -20,14 +20,14 @@ export declare function parseMultipart(message: Buffer | Iterable<Buffer>, optio
|
|
|
20
20
|
*
|
|
21
21
|
* @param stream A Node.js `Readable` stream containing multipart data
|
|
22
22
|
* @param options Options for the parser
|
|
23
|
-
* @
|
|
23
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
24
24
|
*/
|
|
25
25
|
export declare function parseMultipartStream(stream: Readable, options: ParseMultipartOptions): AsyncGenerator<MultipartPart, void, unknown>;
|
|
26
26
|
/**
|
|
27
27
|
* Returns true if the given request is a multipart request.
|
|
28
28
|
*
|
|
29
29
|
* @param req The Node.js `http.IncomingMessage` object to check
|
|
30
|
-
* @
|
|
30
|
+
* @returns `true` if the request is a multipart request, `false` otherwise
|
|
31
31
|
*/
|
|
32
32
|
export declare function isMultipartRequest(req: http.IncomingMessage): boolean;
|
|
33
33
|
/**
|
|
@@ -35,7 +35,7 @@ export declare function isMultipartRequest(req: http.IncomingMessage): boolean;
|
|
|
35
35
|
*
|
|
36
36
|
* @param req The Node.js `http.IncomingMessage` object containing multipart data
|
|
37
37
|
* @param options Options for the parser
|
|
38
|
-
* @
|
|
38
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
39
39
|
*/
|
|
40
40
|
export declare function parseMultipartRequest(req: http.IncomingMessage, options?: MultipartParserOptions): AsyncGenerator<MultipartPart, void, unknown>;
|
|
41
41
|
//# sourceMappingURL=multipart.node.d.ts.map
|
|
@@ -9,7 +9,7 @@ import { getMultipartBoundary } from "./multipart-request.js";
|
|
|
9
9
|
*
|
|
10
10
|
* @param message The multipart message as a `Buffer` or an iterable of `Buffer` chunks
|
|
11
11
|
* @param options Options for the parser
|
|
12
|
-
* @
|
|
12
|
+
* @returns A generator yielding `MultipartPart` objects
|
|
13
13
|
*/
|
|
14
14
|
export function* parseMultipart(message, options) {
|
|
15
15
|
yield* parseMultipartWeb(message, options);
|
|
@@ -22,7 +22,7 @@ export function* parseMultipart(message, options) {
|
|
|
22
22
|
*
|
|
23
23
|
* @param stream A Node.js `Readable` stream containing multipart data
|
|
24
24
|
* @param options Options for the parser
|
|
25
|
-
* @
|
|
25
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
26
26
|
*/
|
|
27
27
|
export async function* parseMultipartStream(stream, options) {
|
|
28
28
|
yield* parseMultipartStreamWeb(Readable.toWeb(stream), options);
|
|
@@ -31,7 +31,7 @@ export async function* parseMultipartStream(stream, options) {
|
|
|
31
31
|
* Returns true if the given request is a multipart request.
|
|
32
32
|
*
|
|
33
33
|
* @param req The Node.js `http.IncomingMessage` object to check
|
|
34
|
-
* @
|
|
34
|
+
* @returns `true` if the request is a multipart request, `false` otherwise
|
|
35
35
|
*/
|
|
36
36
|
export function isMultipartRequest(req) {
|
|
37
37
|
let contentType = req.headers['content-type'];
|
|
@@ -42,7 +42,7 @@ export function isMultipartRequest(req) {
|
|
|
42
42
|
*
|
|
43
43
|
* @param req The Node.js `http.IncomingMessage` object containing multipart data
|
|
44
44
|
* @param options Options for the parser
|
|
45
|
-
* @
|
|
45
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
46
46
|
*/
|
|
47
47
|
export async function* parseMultipartRequest(req, options) {
|
|
48
48
|
if (!isMultipartRequest(req)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/multipart-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "A fast, efficient parser for multipart streams in any JavaScript environment",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"repository": {
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
},
|
|
30
30
|
"./package.json": "./package.json"
|
|
31
31
|
},
|
|
32
|
-
"
|
|
33
|
-
"@remix-run/headers": "^0.
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@remix-run/headers": "^0.19.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^24.6.0",
|
|
37
|
-
"typescript": "
|
|
37
|
+
"@typescript/native-preview": "7.0.0-dev.20251125.1"
|
|
38
38
|
},
|
|
39
39
|
"keywords": [
|
|
40
40
|
"multipart",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"bench:bun": "bun run ./bench/runner.ts",
|
|
48
48
|
"bench:deno": "deno run --allow-sys ./bench/runner.ts",
|
|
49
49
|
"bench:node": "node --disable-warning=ExperimentalWarning ./bench/runner.ts",
|
|
50
|
-
"build": "
|
|
50
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
51
51
|
"clean": "git clean -fdX",
|
|
52
|
-
"test": "node --disable-warning=ExperimentalWarning --test
|
|
53
|
-
"typecheck": "
|
|
52
|
+
"test": "node --disable-warning=ExperimentalWarning --test",
|
|
53
|
+
"typecheck": "tsgo --noEmit"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/lib/buffer-search.ts
CHANGED
|
@@ -6,9 +6,12 @@ export function createSearch(pattern: string): SearchFunction {
|
|
|
6
6
|
let needle = new TextEncoder().encode(pattern)
|
|
7
7
|
|
|
8
8
|
let search: SearchFunction
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Use the built-in Buffer.indexOf method on Node.js for better perf.
|
|
10
|
+
let BufferClass = (globalThis as any).Buffer as
|
|
11
|
+
| { prototype: { indexOf(this: Uint8Array, needle: Uint8Array, start: number): number } }
|
|
12
|
+
| undefined
|
|
13
|
+
if (BufferClass && !('Bun' in globalThis || 'Deno' in globalThis)) {
|
|
14
|
+
search = (haystack, start = 0) => BufferClass.prototype.indexOf.call(haystack, needle, start)
|
|
12
15
|
} else {
|
|
13
16
|
let needleEnd = needle.length - 1
|
|
14
17
|
let skipTable = new Uint8Array(256).fill(needle.length)
|
|
@@ -5,7 +5,7 @@ import { MultipartParseError, parseMultipartStream } from './multipart.ts'
|
|
|
5
5
|
* Extracts the boundary string from a `multipart/*` content type.
|
|
6
6
|
*
|
|
7
7
|
* @param contentType The `Content-Type` header value from the request
|
|
8
|
-
* @
|
|
8
|
+
* @returns The boundary string if found, or null if not present
|
|
9
9
|
*/
|
|
10
10
|
export function getMultipartBoundary(contentType: string): string | null {
|
|
11
11
|
let match = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType)
|
|
@@ -16,7 +16,7 @@ export function getMultipartBoundary(contentType: string): string | null {
|
|
|
16
16
|
* Returns true if the given request contains multipart data.
|
|
17
17
|
*
|
|
18
18
|
* @param request The `Request` object to check
|
|
19
|
-
* @
|
|
19
|
+
* @returns `true` if the request is a multipart request, `false` otherwise
|
|
20
20
|
*/
|
|
21
21
|
export function isMultipartRequest(request: Request): boolean {
|
|
22
22
|
let contentType = request.headers.get('Content-Type')
|
|
@@ -29,7 +29,7 @@ export function isMultipartRequest(request: Request): boolean {
|
|
|
29
29
|
*
|
|
30
30
|
* @param request The `Request` object containing multipart data
|
|
31
31
|
* @param options Optional parser options, such as `maxHeaderSize` and `maxFileSize`
|
|
32
|
-
* @
|
|
32
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
33
33
|
*/
|
|
34
34
|
export async function* parseMultipartRequest(
|
|
35
35
|
request: Request,
|
|
@@ -17,7 +17,7 @@ import { getMultipartBoundary } from './multipart-request.ts'
|
|
|
17
17
|
*
|
|
18
18
|
* @param message The multipart message as a `Buffer` or an iterable of `Buffer` chunks
|
|
19
19
|
* @param options Options for the parser
|
|
20
|
-
* @
|
|
20
|
+
* @returns A generator yielding `MultipartPart` objects
|
|
21
21
|
*/
|
|
22
22
|
export function* parseMultipart(
|
|
23
23
|
message: Buffer | Iterable<Buffer>,
|
|
@@ -34,7 +34,7 @@ export function* parseMultipart(
|
|
|
34
34
|
*
|
|
35
35
|
* @param stream A Node.js `Readable` stream containing multipart data
|
|
36
36
|
* @param options Options for the parser
|
|
37
|
-
* @
|
|
37
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
38
38
|
*/
|
|
39
39
|
export async function* parseMultipartStream(
|
|
40
40
|
stream: Readable,
|
|
@@ -47,7 +47,7 @@ export async function* parseMultipartStream(
|
|
|
47
47
|
* Returns true if the given request is a multipart request.
|
|
48
48
|
*
|
|
49
49
|
* @param req The Node.js `http.IncomingMessage` object to check
|
|
50
|
-
* @
|
|
50
|
+
* @returns `true` if the request is a multipart request, `false` otherwise
|
|
51
51
|
*/
|
|
52
52
|
export function isMultipartRequest(req: http.IncomingMessage): boolean {
|
|
53
53
|
let contentType = req.headers['content-type']
|
|
@@ -59,7 +59,7 @@ export function isMultipartRequest(req: http.IncomingMessage): boolean {
|
|
|
59
59
|
*
|
|
60
60
|
* @param req The Node.js `http.IncomingMessage` object containing multipart data
|
|
61
61
|
* @param options Options for the parser
|
|
62
|
-
* @
|
|
62
|
+
* @returns An async generator yielding `MultipartPart` objects
|
|
63
63
|
*/
|
|
64
64
|
export async function* parseMultipartRequest(
|
|
65
65
|
req: http.IncomingMessage,
|
package/src/lib/multipart.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { ContentDisposition, ContentType, parse as parseRawHeaders } from '@remix-run/headers'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createSearch,
|
|
5
|
+
createPartialTailSearch,
|
|
6
|
+
type SearchFunction,
|
|
7
|
+
type PartialTailSearchFunction,
|
|
8
|
+
} from './buffer-search.ts'
|
|
3
9
|
import { readStream } from './read-stream.ts'
|
|
4
|
-
import type { SearchFunction, PartialTailSearchFunction } from './buffer-search.ts'
|
|
5
|
-
import { createSearch, createPartialTailSearch } from './buffer-search.ts'
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* The base class for errors thrown by the multipart parser.
|
|
9
13
|
*/
|
|
10
14
|
export class MultipartParseError extends Error {
|
|
15
|
+
/**
|
|
16
|
+
* @param message The error message
|
|
17
|
+
*/
|
|
11
18
|
constructor(message: string) {
|
|
12
19
|
super(message)
|
|
13
20
|
this.name = 'MultipartParseError'
|
|
@@ -18,6 +25,9 @@ export class MultipartParseError extends Error {
|
|
|
18
25
|
* An error thrown when the maximum allowed size of a header is exceeded.
|
|
19
26
|
*/
|
|
20
27
|
export class MaxHeaderSizeExceededError extends MultipartParseError {
|
|
28
|
+
/**
|
|
29
|
+
* @param maxHeaderSize The maximum header size that was exceeded
|
|
30
|
+
*/
|
|
21
31
|
constructor(maxHeaderSize: number) {
|
|
22
32
|
super(`Multipart header size exceeds maximum allowed size of ${maxHeaderSize} bytes`)
|
|
23
33
|
this.name = 'MaxHeaderSizeExceededError'
|
|
@@ -28,12 +38,18 @@ export class MaxHeaderSizeExceededError extends MultipartParseError {
|
|
|
28
38
|
* An error thrown when the maximum allowed size of a file is exceeded.
|
|
29
39
|
*/
|
|
30
40
|
export class MaxFileSizeExceededError extends MultipartParseError {
|
|
41
|
+
/**
|
|
42
|
+
* @param maxFileSize The maximum file size that was exceeded
|
|
43
|
+
*/
|
|
31
44
|
constructor(maxFileSize: number) {
|
|
32
45
|
super(`File size exceeds maximum allowed size of ${maxFileSize} bytes`)
|
|
33
46
|
this.name = 'MaxFileSizeExceededError'
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Options for parsing a multipart message.
|
|
52
|
+
*/
|
|
37
53
|
export interface ParseMultipartOptions {
|
|
38
54
|
/**
|
|
39
55
|
* The boundary string used to separate parts in the multipart message,
|
|
@@ -44,14 +60,14 @@ export interface ParseMultipartOptions {
|
|
|
44
60
|
* The maximum allowed size of a header in bytes. If an individual part's header
|
|
45
61
|
* exceeds this size, a `MaxHeaderSizeExceededError` will be thrown.
|
|
46
62
|
*
|
|
47
|
-
*
|
|
63
|
+
* @default 8192 (8 KiB)
|
|
48
64
|
*/
|
|
49
65
|
maxHeaderSize?: number
|
|
50
66
|
/**
|
|
51
67
|
* The maximum allowed size of a file in bytes. If an individual part's content
|
|
52
68
|
* exceeds this size, a `MaxFileSizeExceededError` will be thrown.
|
|
53
69
|
*
|
|
54
|
-
*
|
|
70
|
+
* @default 2097152 (2 MiB)
|
|
55
71
|
*/
|
|
56
72
|
maxFileSize?: number
|
|
57
73
|
}
|
|
@@ -64,7 +80,7 @@ export interface ParseMultipartOptions {
|
|
|
64
80
|
*
|
|
65
81
|
* @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks
|
|
66
82
|
* @param options Options for the parser
|
|
67
|
-
* @
|
|
83
|
+
* @returns A generator that yields `MultipartPart` objects
|
|
68
84
|
*/
|
|
69
85
|
export function* parseMultipart(
|
|
70
86
|
message: Uint8Array | Iterable<Uint8Array>,
|
|
@@ -98,7 +114,7 @@ export function* parseMultipart(
|
|
|
98
114
|
*
|
|
99
115
|
* @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`
|
|
100
116
|
* @param options Options for the parser
|
|
101
|
-
* @
|
|
117
|
+
* @returns An async generator that yields `MultipartPart` objects
|
|
102
118
|
*/
|
|
103
119
|
export async function* parseMultipartStream(
|
|
104
120
|
stream: ReadableStream<Uint8Array>,
|
|
@@ -120,6 +136,9 @@ export async function* parseMultipartStream(
|
|
|
120
136
|
parser.finish()
|
|
121
137
|
}
|
|
122
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Options for configuring a `MultipartParser`.
|
|
141
|
+
*/
|
|
123
142
|
export type MultipartParserOptions = Omit<ParseMultipartOptions, 'boundary'>
|
|
124
143
|
|
|
125
144
|
const MultipartParserStateStart = 0
|
|
@@ -152,6 +171,10 @@ export class MultipartParser {
|
|
|
152
171
|
#currentPart: MultipartPart | null = null
|
|
153
172
|
#contentLength = 0
|
|
154
173
|
|
|
174
|
+
/**
|
|
175
|
+
* @param boundary The boundary string used to separate parts
|
|
176
|
+
* @param options Options for the parser
|
|
177
|
+
*/
|
|
155
178
|
constructor(boundary: string, options?: MultipartParserOptions) {
|
|
156
179
|
this.boundary = boundary
|
|
157
180
|
this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb
|
|
@@ -168,7 +191,7 @@ export class MultipartParser {
|
|
|
168
191
|
* Write a chunk of data to the parser.
|
|
169
192
|
*
|
|
170
193
|
* @param chunk A chunk of data to write to the parser
|
|
171
|
-
* @
|
|
194
|
+
* @returns A generator yielding `MultipartPart` objects as they are parsed
|
|
172
195
|
*/
|
|
173
196
|
*write(chunk: Uint8Array): Generator<MultipartPart, void, unknown> {
|
|
174
197
|
if (this.#state === MultipartParserStateDone) {
|
|
@@ -297,8 +320,6 @@ export class MultipartParser {
|
|
|
297
320
|
*
|
|
298
321
|
* Note: This will throw if the multipart message is incomplete or
|
|
299
322
|
* wasn't properly terminated.
|
|
300
|
-
*
|
|
301
|
-
* @return void
|
|
302
323
|
*/
|
|
303
324
|
finish(): void {
|
|
304
325
|
if (this.#state !== MultipartParserStateDone) {
|
|
@@ -321,6 +342,10 @@ export class MultipartPart {
|
|
|
321
342
|
#header: Uint8Array
|
|
322
343
|
#headers?: Headers
|
|
323
344
|
|
|
345
|
+
/**
|
|
346
|
+
* @param header The raw header bytes
|
|
347
|
+
* @param content The content chunks
|
|
348
|
+
*/
|
|
324
349
|
constructor(header: Uint8Array, content: Uint8Array[]) {
|
|
325
350
|
this.#header = header
|
|
326
351
|
this.content = content
|
|
@@ -354,7 +379,7 @@ export class MultipartPart {
|
|
|
354
379
|
*/
|
|
355
380
|
get headers(): Headers {
|
|
356
381
|
if (!this.#headers) {
|
|
357
|
-
this.#headers =
|
|
382
|
+
this.#headers = parseRawHeaders(decoder.decode(this.#header))
|
|
358
383
|
}
|
|
359
384
|
|
|
360
385
|
return this.#headers
|
|
@@ -378,21 +403,21 @@ export class MultipartPart {
|
|
|
378
403
|
* The filename of the part, if it is a file upload.
|
|
379
404
|
*/
|
|
380
405
|
get filename(): string | undefined {
|
|
381
|
-
return this.headers.
|
|
406
|
+
return ContentDisposition.from(this.headers.get('content-disposition')).preferredFilename
|
|
382
407
|
}
|
|
383
408
|
|
|
384
409
|
/**
|
|
385
410
|
* The media type of the part.
|
|
386
411
|
*/
|
|
387
412
|
get mediaType(): string | undefined {
|
|
388
|
-
return this.headers.
|
|
413
|
+
return ContentType.from(this.headers.get('content-type')).mediaType
|
|
389
414
|
}
|
|
390
415
|
|
|
391
416
|
/**
|
|
392
417
|
* The name of the part, usually the `name` of the field in the `<form>` that submitted the request.
|
|
393
418
|
*/
|
|
394
419
|
get name(): string | undefined {
|
|
395
|
-
return this.headers.
|
|
420
|
+
return ContentDisposition.from(this.headers.get('content-disposition')).name
|
|
396
421
|
}
|
|
397
422
|
|
|
398
423
|
/**
|