@remix-run/multipart-parser 0.11.0 → 0.13.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.
Files changed (35) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +39 -45
  3. package/dist/{multipart-parser.d.ts → index.d.ts} +1 -1
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/lib/buffer-search.d.ts.map +1 -1
  7. package/dist/lib/buffer-search.js +51 -0
  8. package/dist/lib/multipart-request.d.ts.map +1 -1
  9. package/dist/lib/multipart-request.js +46 -0
  10. package/dist/lib/multipart.d.ts.map +1 -1
  11. package/dist/lib/multipart.js +320 -0
  12. package/dist/lib/multipart.node.d.ts.map +1 -1
  13. package/dist/lib/multipart.node.js +60 -0
  14. package/dist/lib/read-stream.js +16 -0
  15. package/dist/{multipart-parser.node.d.ts → node.d.ts} +2 -2
  16. package/dist/node.d.ts.map +1 -0
  17. package/dist/node.js +4 -0
  18. package/package.json +13 -24
  19. package/src/{multipart-parser.ts → index.ts} +3 -3
  20. package/src/lib/buffer-search.ts +25 -25
  21. package/src/lib/multipart-request.ts +11 -11
  22. package/src/lib/multipart.node.ts +13 -13
  23. package/src/lib/multipart.ts +123 -123
  24. package/src/lib/read-stream.ts +5 -5
  25. package/src/{multipart-parser.node.ts → node.ts} +9 -4
  26. package/dist/multipart-parser.cjs +0 -2021
  27. package/dist/multipart-parser.cjs.map +0 -7
  28. package/dist/multipart-parser.d.ts.map +0 -1
  29. package/dist/multipart-parser.js +0 -1985
  30. package/dist/multipart-parser.js.map +0 -7
  31. package/dist/multipart-parser.node.cjs +0 -2027
  32. package/dist/multipart-parser.node.cjs.map +0 -7
  33. package/dist/multipart-parser.node.d.ts.map +0 -1
  34. package/dist/multipart-parser.node.js +0 -1991
  35. package/dist/multipart-parser.node.js.map +0 -7
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Michael Jackson
3
+ Copyright (c) 2025 Shopify Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
  - Convenient `MultipartPart` API with `arrayBuffer`, `bytes`, `text`, `size`, and metadata access
20
20
  - Built-in file size limiting to prevent abuse
21
21
  - First-class Node.js support with native `http.IncomingMessage` compatibility
22
- - [Examples for every major runtime](https://github.com/remix-run/remix/tree/v3/packages/multipart-parser/examples)
22
+ - [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
23
23
 
24
24
  ## Installation
25
25
 
@@ -29,41 +29,35 @@ Install from [npm](https://www.npmjs.com/):
29
29
  npm install @remix-run/multipart-parser
30
30
  ```
31
31
 
32
- Or install from [JSR](https://jsr.io/):
33
-
34
- ```sh
35
- deno add @remix-run/multipart-parser
36
- ```
37
-
38
32
  ## Usage
39
33
 
40
34
  The most common use case for `multipart-parser` is handling file uploads when you're building a web server. For this case, the `parseMultipartRequest` function is your friend. It automatically validates the request is `multipart/form-data`, extracts the multipart boundary from the `Content-Type` header, parses all fields and files in the `request.body` stream, and gives each one to you as a `MultipartPart` object with a rich API for accessing its metadata and content.
41
35
 
42
36
  ```ts
43
- import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser';
37
+ import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser'
44
38
 
45
39
  async function handleRequest(request: Request): void {
46
40
  try {
47
41
  for await (let part of parseMultipartRequest(request)) {
48
42
  if (part.isFile) {
49
43
  // Access file data in multiple formats
50
- let buffer = part.arrayBuffer; // ArrayBuffer
51
- console.log(`File received: ${part.filename} (${buffer.byteLength} bytes)`);
52
- console.log(`Content type: ${part.mediaType}`);
53
- console.log(`Field name: ${part.name}`);
44
+ let buffer = part.arrayBuffer // ArrayBuffer
45
+ console.log(`File received: ${part.filename} (${buffer.byteLength} bytes)`)
46
+ console.log(`Content type: ${part.mediaType}`)
47
+ console.log(`Field name: ${part.name}`)
54
48
 
55
49
  // Save to disk, upload to cloud storage, etc.
56
- await saveFile(part.filename, part.bytes);
50
+ await saveFile(part.filename, part.bytes)
57
51
  } else {
58
- let text = part.text; // string
59
- console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`);
52
+ let text = part.text // string
53
+ console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
60
54
  }
61
55
  }
62
56
  } catch (error) {
63
57
  if (error instanceof MultipartParseError) {
64
- console.error('Failed to parse multipart request:', error.message);
58
+ console.error('Failed to parse multipart request:', error.message)
65
59
  } else {
66
- console.error('An unexpected error occurred:', error);
60
+ console.error('An unexpected error occurred:', error)
67
61
  }
68
62
  }
69
63
  }
@@ -78,10 +72,10 @@ import {
78
72
  MultipartParseError,
79
73
  MaxFileSizeExceededError,
80
74
  parseMultipartRequest,
81
- } from '@remix-run/multipart-parser/node';
75
+ } from '@remix-run/multipart-parser/node'
82
76
 
83
- const oneMb = Math.pow(2, 20);
84
- const maxFileSize = 10 * oneMb;
77
+ const oneMb = Math.pow(2, 20)
78
+ const maxFileSize = 10 * oneMb
85
79
 
86
80
  async function handleRequest(request: Request): Promise<Response> {
87
81
  try {
@@ -90,12 +84,12 @@ async function handleRequest(request: Request): Promise<Response> {
90
84
  }
91
85
  } catch (error) {
92
86
  if (error instanceof MaxFileSizeExceededError) {
93
- return new Response('File size limit exceeded', { status: 413 });
87
+ return new Response('File size limit exceeded', { status: 413 })
94
88
  } else if (error instanceof MultipartParseError) {
95
- return new Response('Failed to parse multipart request', { status: 400 });
89
+ return new Response('Failed to parse multipart request', { status: 400 })
96
90
  } else {
97
- console.error(error);
98
- return new Response('Internal Server Error', { status: 500 });
91
+ console.error(error)
92
+ return new Response('Internal Server Error', { status: 500 })
99
93
  }
100
94
  }
101
95
  }
@@ -108,8 +102,8 @@ The main module (`import from "@remix-run/multipart-parser"`) assumes you're wor
108
102
  If however you're building a server for Node.js that relies on node-specific APIs like `http.IncomingMessage`, `stream.Readable`, and `buffer.Buffer` (ala Express or `http.createServer`), `multipart-parser` ships with an additional module that works directly with these APIs.
109
103
 
110
104
  ```ts
111
- import * as http from 'node:http';
112
- import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser/node';
105
+ import * as http from 'node:http'
106
+ import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser/node'
113
107
 
114
108
  let server = http.createServer(async (req, res) => {
115
109
  try {
@@ -118,14 +112,14 @@ let server = http.createServer(async (req, res) => {
118
112
  }
119
113
  } catch (error) {
120
114
  if (error instanceof MultipartParseError) {
121
- console.error('Failed to parse multipart request:', error.message);
115
+ console.error('Failed to parse multipart request:', error.message)
122
116
  } else {
123
- console.error('An unexpected error occurred:', error);
117
+ console.error('An unexpected error occurred:', error)
124
118
  }
125
119
  }
126
- });
120
+ })
127
121
 
128
- server.listen(8080);
122
+ server.listen(8080)
129
123
  ```
130
124
 
131
125
  ## Low-level API
@@ -133,10 +127,10 @@ server.listen(8080);
133
127
  If you're working directly with multipart boundaries and buffers/streams of multipart data that are not necessarily part of a request, `multipart-parser` provides a low-level `parseMultipart()` API that you can use directly:
134
128
 
135
129
  ```ts
136
- import { parseMultipart } from '@remix-run/multipart-parser';
130
+ import { parseMultipart } from '@remix-run/multipart-parser'
137
131
 
138
- let message = new Uint8Array(/* ... */);
139
- let boundary = '----WebKitFormBoundary56eac3x';
132
+ let message = new Uint8Array(/* ... */)
133
+ let boundary = '----WebKitFormBoundary56eac3x'
140
134
 
141
135
  for (let part of parseMultipart(message, { boundary })) {
142
136
  // ...
@@ -146,24 +140,24 @@ for (let part of parseMultipart(message, { boundary })) {
146
140
  In addition, the `parseMultipartStream` function provides an `async` generator interface for multipart data in a `ReadableStream`:
147
141
 
148
142
  ```ts
149
- import { parseMultipartStream } from '@remix-run/multipart-parser';
143
+ import { parseMultipartStream } from '@remix-run/multipart-parser'
150
144
 
151
- let message = new ReadableStream(/* ... */);
152
- let boundary = '----WebKitFormBoundary56eac3x';
145
+ let message = new ReadableStream(/* ... */)
146
+ let boundary = '----WebKitFormBoundary56eac3x'
153
147
 
154
148
  for await (let part of parseMultipartStream(message, { boundary })) {
155
149
  // ...
156
150
  }
157
151
  ```
158
152
 
159
- ## Examples
153
+ ## Demos
160
154
 
161
- The [`examples` directory](https://github.com/remix-run/remix/tree/v3/packages/multipart-parser/examples) contains a few working examples of how you can use this library:
155
+ The [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos) contains a few working demos of how you can use this library:
162
156
 
163
- - [`examples/bun`](https://github.com/remix-run/remix/tree/v3/packages/multipart-parser/examples/bun) - using multipart-parser in Bun
164
- - [`examples/cf-workers`](https://github.com/remix-run/remix/tree/v3/packages/multipart-parser/examples/cf-workers) - using multipart-parser in a Cloudflare Worker and storing file uploads in R2
165
- - [`examples/deno`](https://github.com/remix-run/remix/tree/v3/packages/multipart-parser/examples/deno) - using multipart-parser in Deno
166
- - [`examples/node`](https://github.com/remix-run/remix/tree/v3/packages/multipart-parser/examples/node) - using multipart-parser in Node.js
157
+ - [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) - using multipart-parser in Bun
158
+ - [`demos/cf-workers`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/cf-workers) - using multipart-parser in a Cloudflare Worker and storing file uploads in R2
159
+ - [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) - using multipart-parser in Deno
160
+ - [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) - using multipart-parser in Node.js
167
161
 
168
162
  ## Benchmark
169
163
 
@@ -223,8 +217,8 @@ Deno 2.3.6
223
217
 
224
218
  ## Related Packages
225
219
 
226
- - [`form-data-parser`](https://github.com/remix-run/remix/tree/v3/packages/form-data-parser) - Uses `multipart-parser` internally to parse multipart requests and generate `FileUpload`s for storage
227
- - [`headers`](https://github.com/remix-run/remix/tree/v3/packages/headers) - Used internally to parse HTTP headers and get metadata (filename, content type) for each `MultipartPart`
220
+ - [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - Uses `multipart-parser` internally to parse multipart requests and generate `FileUpload`s for storage
221
+ - [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Used internally to parse HTTP headers and get metadata (filename, content type) for each `MultipartPart`
228
222
 
229
223
  ## Credits
230
224
 
@@ -232,4 +226,4 @@ Thanks to Jacob Ebey who gave me several code reviews on this project prior to p
232
226
 
233
227
  ## License
234
228
 
235
- See [LICENSE](https://github.com/remix-run/remix/blob/v3/LICENSE)
229
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -1,4 +1,4 @@
1
1
  export type { ParseMultipartOptions, MultipartParserOptions } from './lib/multipart.ts';
2
2
  export { MultipartParseError, MaxHeaderSizeExceededError, MaxFileSizeExceededError, parseMultipart, parseMultipartStream, MultipartParser, MultipartPart, } from './lib/multipart.ts';
3
3
  export { getMultipartBoundary, isMultipartRequest, parseMultipartRequest, } from './lib/multipart-request.ts';
4
- //# sourceMappingURL=multipart-parser.d.ts.map
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACvF,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,aAAa,GACd,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,4BAA4B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { MultipartParseError, MaxHeaderSizeExceededError, MaxFileSizeExceededError, parseMultipart, parseMultipartStream, MultipartParser, MultipartPart, } from "./lib/multipart.js";
2
+ export { getMultipartBoundary, isMultipartRequest, parseMultipartRequest, } from "./lib/multipart-request.js";
@@ -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,CAAC;CAChD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA+B5D;AAED,MAAM,WAAW,yBAAyB;IACxC,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC;CAChC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,yBAAyB,CAyBlF"}
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,CA+B5D;AAED,MAAM,WAAW,yBAAyB;IACxC,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAA;CAC/B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,yBAAyB,CAyBlF"}
@@ -0,0 +1,51 @@
1
+ export function createSearch(pattern) {
2
+ let needle = new TextEncoder().encode(pattern);
3
+ let search;
4
+ if ('Buffer' in globalThis && !('Bun' in globalThis || 'Deno' in globalThis)) {
5
+ // Use the built-in Buffer.indexOf method on Node.js for better perf.
6
+ search = (haystack, start = 0) => Buffer.prototype.indexOf.call(haystack, needle, start);
7
+ }
8
+ else {
9
+ let needleEnd = needle.length - 1;
10
+ let skipTable = new Uint8Array(256).fill(needle.length);
11
+ for (let i = 0; i < needleEnd; ++i) {
12
+ skipTable[needle[i]] = needleEnd - i;
13
+ }
14
+ search = (haystack, start = 0) => {
15
+ let haystackLength = haystack.length;
16
+ let i = start + needleEnd;
17
+ while (i < haystackLength) {
18
+ for (let j = needleEnd, k = i; j >= 0 && haystack[k] === needle[j]; --j, --k) {
19
+ if (j === 0)
20
+ return k;
21
+ }
22
+ i += skipTable[haystack[i]];
23
+ }
24
+ return -1;
25
+ };
26
+ }
27
+ return search;
28
+ }
29
+ export function createPartialTailSearch(pattern) {
30
+ let needle = new TextEncoder().encode(pattern);
31
+ let byteIndexes = {};
32
+ for (let i = 0; i < needle.length; ++i) {
33
+ let byte = needle[i];
34
+ if (byteIndexes[byte] === undefined)
35
+ byteIndexes[byte] = [];
36
+ byteIndexes[byte].push(i);
37
+ }
38
+ return function (haystack) {
39
+ let haystackEnd = haystack.length - 1;
40
+ if (haystack[haystackEnd] in byteIndexes) {
41
+ let indexes = byteIndexes[haystack[haystackEnd]];
42
+ for (let i = indexes.length - 1; i >= 0; --i) {
43
+ for (let j = indexes[i], k = haystackEnd; j >= 0 && haystack[k] === needle[j]; --j, --k) {
44
+ if (j === 0)
45
+ return k;
46
+ }
47
+ }
48
+ }
49
+ return -1;
50
+ };
51
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"multipart-request.d.ts","sourceRoot":"","sources":["../../src/lib/multipart-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG5E;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAG5D;AAED;;;;;;;GAOG;AACH,wBAAuB,qBAAqB,CAC1C,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAkB9C"}
1
+ {"version":3,"file":"multipart-request.d.ts","sourceRoot":"","sources":["../../src/lib/multipart-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAG3E;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAG5D;AAED;;;;;;;GAOG;AACH,wBAAuB,qBAAqB,CAC1C,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAkB9C"}
@@ -0,0 +1,46 @@
1
+ import { MultipartParseError, parseMultipartStream } from "./multipart.js";
2
+ /**
3
+ * Extracts the boundary string from a `multipart/*` content type.
4
+ *
5
+ * @param contentType The `Content-Type` header value from the request
6
+ * @return The boundary string if found, or null if not present
7
+ */
8
+ export function getMultipartBoundary(contentType) {
9
+ let match = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType);
10
+ return match ? (match[1] ?? match[2]) : null;
11
+ }
12
+ /**
13
+ * Returns true if the given request contains multipart data.
14
+ *
15
+ * @param request The `Request` object to check
16
+ * @return `true` if the request is a multipart request, `false` otherwise
17
+ */
18
+ export function isMultipartRequest(request) {
19
+ let contentType = request.headers.get('Content-Type');
20
+ return contentType != null && contentType.startsWith('multipart/');
21
+ }
22
+ /**
23
+ * Parse a multipart [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and yield each part as
24
+ * a `MultipartPart` object. Useful in HTTP server contexts for handling incoming `multipart/*` requests.
25
+ *
26
+ * @param request The `Request` object containing multipart data
27
+ * @param options Optional parser options, such as `maxHeaderSize` and `maxFileSize`
28
+ * @return An async generator yielding `MultipartPart` objects
29
+ */
30
+ export async function* parseMultipartRequest(request, options) {
31
+ if (!isMultipartRequest(request)) {
32
+ throw new MultipartParseError('Request is not a multipart request');
33
+ }
34
+ if (!request.body) {
35
+ throw new MultipartParseError('Request body is empty');
36
+ }
37
+ let boundary = getMultipartBoundary(request.headers.get('Content-Type'));
38
+ if (!boundary) {
39
+ throw new MultipartParseError('Invalid Content-Type header: missing boundary');
40
+ }
41
+ yield* parseMultipartStream(request.body, {
42
+ boundary,
43
+ maxHeaderSize: options?.maxHeaderSize,
44
+ maxFileSize: options?.maxFileSize,
45
+ });
46
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,oBAAoB,CAAC;AAMzC;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;gBACrD,aAAa,EAAE,MAAM;CAIlC;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB;gBACnD,WAAW,EAAE,MAAM;CAIhC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;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,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,CAAC;AAa7E;;GAEG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBAajB,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAY9D;;;;;OAKG;IACF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC;IA0HlE;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;CAKf;AAID;;GAEG;AACH,qBAAa,aAAa;;IACxB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;gBAKnB,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE;IAKrD;;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"}
1
+ {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,oBAAoB,CAAA;AAMxC;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;gBACrD,aAAa,EAAE,MAAM;CAIlC;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB;gBACnD,WAAW,EAAE,MAAM;CAIhC;AAED,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,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;gBAahB,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAY9D;;;;;OAKG;IACF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC;IA0HlE;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;CAKf;AAID;;GAEG;AACH,qBAAa,aAAa;;IACxB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAA;gBAKlB,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE;IAKrD;;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"}
@@ -0,0 +1,320 @@
1
+ import Headers from '@remix-run/headers';
2
+ import { readStream } from "./read-stream.js";
3
+ import { createSearch, createPartialTailSearch } from "./buffer-search.js";
4
+ /**
5
+ * The base class for errors thrown by the multipart parser.
6
+ */
7
+ export class MultipartParseError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'MultipartParseError';
11
+ }
12
+ }
13
+ /**
14
+ * An error thrown when the maximum allowed size of a header is exceeded.
15
+ */
16
+ export class MaxHeaderSizeExceededError extends MultipartParseError {
17
+ constructor(maxHeaderSize) {
18
+ super(`Multipart header size exceeds maximum allowed size of ${maxHeaderSize} bytes`);
19
+ this.name = 'MaxHeaderSizeExceededError';
20
+ }
21
+ }
22
+ /**
23
+ * An error thrown when the maximum allowed size of a file is exceeded.
24
+ */
25
+ export class MaxFileSizeExceededError extends MultipartParseError {
26
+ constructor(maxFileSize) {
27
+ super(`File size exceeds maximum allowed size of ${maxFileSize} bytes`);
28
+ this.name = 'MaxFileSizeExceededError';
29
+ }
30
+ }
31
+ /**
32
+ * Parse a `multipart/*` message from a buffer/iterable and yield each part as a `MultipartPart` object.
33
+ *
34
+ * Note: This is a low-level API that requires manual handling of the content and boundary. If you're
35
+ * building a web server, consider using `parseMultipartRequest(request)` instead.
36
+ *
37
+ * @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks
38
+ * @param options Options for the parser
39
+ * @return A generator that yields `MultipartPart` objects
40
+ */
41
+ export function* parseMultipart(message, options) {
42
+ let parser = new MultipartParser(options.boundary, {
43
+ maxHeaderSize: options.maxHeaderSize,
44
+ maxFileSize: options.maxFileSize,
45
+ });
46
+ if (message instanceof Uint8Array) {
47
+ if (message.length === 0) {
48
+ return; // No data to parse
49
+ }
50
+ yield* parser.write(message);
51
+ }
52
+ else {
53
+ for (let chunk of message) {
54
+ yield* parser.write(chunk);
55
+ }
56
+ }
57
+ parser.finish();
58
+ }
59
+ /**
60
+ * Parse a `multipart/*` message stream and yield each part as a `MultipartPart` object.
61
+ *
62
+ * Note: This is a low-level API that requires manual handling of the content and boundary. If you're
63
+ * building a web server, consider using `parseMultipartRequest(request)` instead.
64
+ *
65
+ * @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`
66
+ * @param options Options for the parser
67
+ * @return An async generator that yields `MultipartPart` objects
68
+ */
69
+ export async function* parseMultipartStream(stream, options) {
70
+ let parser = new MultipartParser(options.boundary, {
71
+ maxHeaderSize: options.maxHeaderSize,
72
+ maxFileSize: options.maxFileSize,
73
+ });
74
+ for await (let chunk of readStream(stream)) {
75
+ if (chunk.length === 0) {
76
+ continue; // No data to parse
77
+ }
78
+ yield* parser.write(chunk);
79
+ }
80
+ parser.finish();
81
+ }
82
+ const MultipartParserStateStart = 0;
83
+ const MultipartParserStateAfterBoundary = 1;
84
+ const MultipartParserStateHeader = 2;
85
+ const MultipartParserStateBody = 3;
86
+ const MultipartParserStateDone = 4;
87
+ const findDoubleNewline = createSearch('\r\n\r\n');
88
+ const oneKb = 1024;
89
+ const oneMb = 1024 * oneKb;
90
+ /**
91
+ * A streaming parser for `multipart/*` HTTP messages.
92
+ */
93
+ export class MultipartParser {
94
+ boundary;
95
+ maxHeaderSize;
96
+ maxFileSize;
97
+ #findOpeningBoundary;
98
+ #openingBoundaryLength;
99
+ #findBoundary;
100
+ #findPartialTailBoundary;
101
+ #boundaryLength;
102
+ #state = MultipartParserStateStart;
103
+ #buffer = null;
104
+ #currentPart = null;
105
+ #contentLength = 0;
106
+ constructor(boundary, options) {
107
+ this.boundary = boundary;
108
+ this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb;
109
+ this.maxFileSize = options?.maxFileSize ?? 2 * oneMb;
110
+ this.#findOpeningBoundary = createSearch(`--${boundary}`);
111
+ this.#openingBoundaryLength = 2 + boundary.length; // length of '--' + boundary
112
+ this.#findBoundary = createSearch(`\r\n--${boundary}`);
113
+ this.#findPartialTailBoundary = createPartialTailSearch(`\r\n--${boundary}`);
114
+ this.#boundaryLength = 4 + boundary.length; // length of '\r\n--' + boundary
115
+ }
116
+ /**
117
+ * Write a chunk of data to the parser.
118
+ *
119
+ * @param chunk A chunk of data to write to the parser
120
+ * @return A generator yielding `MultipartPart` objects as they are parsed
121
+ */
122
+ *write(chunk) {
123
+ if (this.#state === MultipartParserStateDone) {
124
+ throw new MultipartParseError('Unexpected data after end of stream');
125
+ }
126
+ let index = 0;
127
+ let chunkLength = chunk.length;
128
+ if (this.#buffer !== null) {
129
+ let newChunk = new Uint8Array(this.#buffer.length + chunkLength);
130
+ newChunk.set(this.#buffer, 0);
131
+ newChunk.set(chunk, this.#buffer.length);
132
+ chunk = newChunk;
133
+ chunkLength = chunk.length;
134
+ this.#buffer = null;
135
+ }
136
+ while (true) {
137
+ if (this.#state === MultipartParserStateBody) {
138
+ if (chunkLength - index < this.#boundaryLength) {
139
+ this.#buffer = chunk.subarray(index);
140
+ break;
141
+ }
142
+ let boundaryIndex = this.#findBoundary(chunk, index);
143
+ if (boundaryIndex === -1) {
144
+ // No boundary found, but there may be a partial match at the end of the chunk.
145
+ let partialTailIndex = this.#findPartialTailBoundary(chunk);
146
+ if (partialTailIndex === -1) {
147
+ this.#append(index === 0 ? chunk : chunk.subarray(index));
148
+ }
149
+ else {
150
+ this.#append(chunk.subarray(index, partialTailIndex));
151
+ this.#buffer = chunk.subarray(partialTailIndex);
152
+ }
153
+ break;
154
+ }
155
+ this.#append(chunk.subarray(index, boundaryIndex));
156
+ yield this.#currentPart;
157
+ index = boundaryIndex + this.#boundaryLength;
158
+ this.#state = MultipartParserStateAfterBoundary;
159
+ }
160
+ if (this.#state === MultipartParserStateAfterBoundary) {
161
+ if (chunkLength - index < 2) {
162
+ this.#buffer = chunk.subarray(index);
163
+ break;
164
+ }
165
+ if (chunk[index] === 45 && chunk[index + 1] === 45) {
166
+ this.#state = MultipartParserStateDone;
167
+ break;
168
+ }
169
+ index += 2; // Skip \r\n after boundary
170
+ this.#state = MultipartParserStateHeader;
171
+ }
172
+ if (this.#state === MultipartParserStateHeader) {
173
+ if (chunkLength - index < 4) {
174
+ this.#buffer = chunk.subarray(index);
175
+ break;
176
+ }
177
+ let headerEndIndex = findDoubleNewline(chunk, index);
178
+ if (headerEndIndex === -1) {
179
+ if (chunkLength - index > this.maxHeaderSize) {
180
+ throw new MaxHeaderSizeExceededError(this.maxHeaderSize);
181
+ }
182
+ this.#buffer = chunk.subarray(index);
183
+ break;
184
+ }
185
+ if (headerEndIndex - index > this.maxHeaderSize) {
186
+ throw new MaxHeaderSizeExceededError(this.maxHeaderSize);
187
+ }
188
+ this.#currentPart = new MultipartPart(chunk.subarray(index, headerEndIndex), []);
189
+ this.#contentLength = 0;
190
+ index = headerEndIndex + 4; // Skip header + \r\n\r\n
191
+ this.#state = MultipartParserStateBody;
192
+ continue;
193
+ }
194
+ if (this.#state === MultipartParserStateStart) {
195
+ if (chunkLength < this.#openingBoundaryLength) {
196
+ this.#buffer = chunk;
197
+ break;
198
+ }
199
+ if (this.#findOpeningBoundary(chunk) !== 0) {
200
+ throw new MultipartParseError('Invalid multipart stream: missing initial boundary');
201
+ }
202
+ index = this.#openingBoundaryLength;
203
+ this.#state = MultipartParserStateAfterBoundary;
204
+ }
205
+ }
206
+ }
207
+ #append(chunk) {
208
+ if (this.#contentLength + chunk.length > this.maxFileSize) {
209
+ throw new MaxFileSizeExceededError(this.maxFileSize);
210
+ }
211
+ this.#currentPart.content.push(chunk);
212
+ this.#contentLength += chunk.length;
213
+ }
214
+ /**
215
+ * Should be called after all data has been written to the parser.
216
+ *
217
+ * Note: This will throw if the multipart message is incomplete or
218
+ * wasn't properly terminated.
219
+ *
220
+ * @return void
221
+ */
222
+ finish() {
223
+ if (this.#state !== MultipartParserStateDone) {
224
+ throw new MultipartParseError('Multipart stream not finished');
225
+ }
226
+ }
227
+ }
228
+ const decoder = new TextDecoder('utf-8', { fatal: true });
229
+ /**
230
+ * A part of a `multipart/*` HTTP message.
231
+ */
232
+ export class MultipartPart {
233
+ /**
234
+ * The raw content of this part as an array of `Uint8Array` chunks.
235
+ */
236
+ content;
237
+ #header;
238
+ #headers;
239
+ constructor(header, content) {
240
+ this.#header = header;
241
+ this.content = content;
242
+ }
243
+ /**
244
+ * The content of this part as an `ArrayBuffer`.
245
+ */
246
+ get arrayBuffer() {
247
+ return this.bytes.buffer;
248
+ }
249
+ /**
250
+ * The content of this part as a single `Uint8Array`. In `multipart/form-data` messages, this is useful
251
+ * for reading the value of files that were uploaded using `<input type="file">` fields.
252
+ */
253
+ get bytes() {
254
+ let buffer = new Uint8Array(this.size);
255
+ let offset = 0;
256
+ for (let chunk of this.content) {
257
+ buffer.set(chunk, offset);
258
+ offset += chunk.length;
259
+ }
260
+ return buffer;
261
+ }
262
+ /**
263
+ * The headers associated with this part.
264
+ */
265
+ get headers() {
266
+ if (!this.#headers) {
267
+ this.#headers = new Headers(decoder.decode(this.#header));
268
+ }
269
+ return this.#headers;
270
+ }
271
+ /**
272
+ * True if this part originated from a file upload.
273
+ */
274
+ get isFile() {
275
+ return this.filename !== undefined || this.mediaType === 'application/octet-stream';
276
+ }
277
+ /**
278
+ * True if this part originated from a text input field in a form submission.
279
+ */
280
+ get isText() {
281
+ return !this.isFile;
282
+ }
283
+ /**
284
+ * The filename of the part, if it is a file upload.
285
+ */
286
+ get filename() {
287
+ return this.headers.contentDisposition.preferredFilename;
288
+ }
289
+ /**
290
+ * The media type of the part.
291
+ */
292
+ get mediaType() {
293
+ return this.headers.contentType.mediaType;
294
+ }
295
+ /**
296
+ * The name of the part, usually the `name` of the field in the `<form>` that submitted the request.
297
+ */
298
+ get name() {
299
+ return this.headers.contentDisposition.name;
300
+ }
301
+ /**
302
+ * The size of the content in bytes.
303
+ */
304
+ get size() {
305
+ let size = 0;
306
+ for (let chunk of this.content) {
307
+ size += chunk.length;
308
+ }
309
+ return size;
310
+ }
311
+ /**
312
+ * The content of this part as a string. In `multipart/form-data` messages, this is useful for
313
+ * reading the value of parts that originated from `<input type="text">` fields.
314
+ *
315
+ * Note: Do not use this for binary data, use `part.bytes` or `part.arrayBuffer` instead.
316
+ */
317
+ get text() {
318
+ return decoder.decode(this.bytes);
319
+ }
320
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"multipart.node.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAQnG;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAEzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAGrE;AAED;;;;;;GAMG;AACH,wBAAuB,qBAAqB,CAC1C,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C"}
1
+ {"version":3,"file":"multipart.node.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAQlG;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAEzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAGrE;AAED;;;;;;GAMG;AACH,wBAAuB,qBAAqB,CAC1C,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C"}