@fluojs/runtime 2.0.1 → 3.0.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.ko.md +278 -20
- package/README.md +278 -20
- package/dist/adapters/internal-request-response-factory.d.ts +1 -1
- package/dist/adapters/internal-request-response-factory.d.ts.map +1 -1
- package/dist/adapters/internal-request-response-factory.js +1 -1
- package/dist/adapters/request-response-factory.d.ts +20 -1
- package/dist/adapters/request-response-factory.d.ts.map +1 -1
- package/dist/adapters/request-response-factory.js +75 -22
- package/dist/bootstrap.d.ts +3 -9
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +179 -75
- package/dist/devtools/contracts.d.ts +3 -135
- package/dist/devtools/contracts.d.ts.map +1 -1
- package/dist/devtools/index.d.ts +2 -3
- package/dist/devtools/index.d.ts.map +1 -1
- package/dist/devtools/index.js +1 -2
- package/dist/devtools/snapshot.d.ts +1 -1
- package/dist/devtools/snapshot.d.ts.map +1 -1
- package/dist/devtools/snapshot.js +18 -17
- package/dist/devtools/studio-request-observer.d.ts +14 -0
- package/dist/devtools/studio-request-observer.d.ts.map +1 -0
- package/dist/devtools/studio-request-observer.js +112 -0
- package/dist/devtools/studio-runtime-config.d.ts +25 -0
- package/dist/devtools/studio-runtime-config.d.ts.map +1 -0
- package/dist/devtools/studio-runtime-config.js +59 -0
- package/dist/devtools/studio-runtime.d.ts +12 -18
- package/dist/devtools/studio-runtime.d.ts.map +1 -1
- package/dist/devtools/studio-runtime.js +25 -169
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +38 -0
- package/dist/health/health.d.ts +6 -2
- package/dist/health/health.d.ts.map +1 -1
- package/dist/health/health.js +3 -2
- package/dist/http-adapter-shared.d.ts +1 -1
- package/dist/http-adapter-shared.d.ts.map +1 -1
- package/dist/http-adapter-shared.js +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/internal/route-inspection-metadata.d.ts +30 -0
- package/dist/internal/route-inspection-metadata.d.ts.map +1 -0
- package/dist/internal/route-inspection-metadata.js +68 -0
- package/dist/internal.d.ts +6 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -1
- package/dist/module-definition.d.ts +10 -0
- package/dist/module-definition.d.ts.map +1 -0
- package/dist/module-definition.js +12 -0
- package/dist/module-graph.d.ts +41 -0
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +108 -13
- package/dist/multipart.d.ts +67 -3
- package/dist/multipart.d.ts.map +1 -1
- package/dist/multipart.js +665 -103
- package/dist/platform-component-registry.d.ts +9 -0
- package/dist/platform-component-registry.d.ts.map +1 -0
- package/dist/platform-component-registry.js +70 -0
- package/dist/platform-diagnostic-retention.d.ts +13 -0
- package/dist/platform-diagnostic-retention.d.ts.map +1 -0
- package/dist/platform-diagnostic-retention.js +43 -0
- package/dist/platform-shell-probe-reports.d.ts +8 -0
- package/dist/platform-shell-probe-reports.d.ts.map +1 -0
- package/dist/platform-shell-probe-reports.js +95 -0
- package/dist/platform-shell-probes.d.ts +16 -0
- package/dist/platform-shell-probes.d.ts.map +1 -0
- package/dist/platform-shell-probes.js +141 -0
- package/dist/platform-shell.d.ts +7 -13
- package/dist/platform-shell.d.ts.map +1 -1
- package/dist/platform-shell.js +58 -310
- package/dist/retryable-shutdown.d.ts +7 -0
- package/dist/retryable-shutdown.d.ts.map +1 -0
- package/dist/retryable-shutdown.js +20 -0
- package/dist/route-inspection.d.ts +56 -0
- package/dist/route-inspection.d.ts.map +1 -0
- package/dist/route-inspection.js +80 -0
- package/dist/tokens.d.ts +6 -4
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +5 -0
- package/dist/types.d.ts +53 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/web.d.ts +22 -3
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +80 -34
- package/package.json +13 -26
- package/dist/internal-node.d.ts +0 -2
- package/dist/internal-node.d.ts.map +0 -1
- package/dist/internal-node.js +0 -1
- package/dist/logging/json-logger.d.ts +0 -8
- package/dist/logging/json-logger.d.ts.map +0 -1
- package/dist/logging/json-logger.js +0 -45
- package/dist/logging/logger.d.ts +0 -37
- package/dist/logging/logger.d.ts.map +0 -1
- package/dist/logging/logger.js +0 -103
- package/dist/node/internal-node-compression.d.ts +0 -22
- package/dist/node/internal-node-compression.d.ts.map +0 -1
- package/dist/node/internal-node-compression.js +0 -84
- package/dist/node/internal-node-request.d.ts +0 -162
- package/dist/node/internal-node-request.d.ts.map +0 -1
- package/dist/node/internal-node-request.js +0 -478
- package/dist/node/internal-node-response.d.ts +0 -28
- package/dist/node/internal-node-response.d.ts.map +0 -1
- package/dist/node/internal-node-response.js +0 -205
- package/dist/node/internal-node-shutdown.d.ts +0 -34
- package/dist/node/internal-node-shutdown.d.ts.map +0 -1
- package/dist/node/internal-node-shutdown.js +0 -83
- package/dist/node/internal-node.d.ts +0 -111
- package/dist/node/internal-node.d.ts.map +0 -1
- package/dist/node/internal-node.js +0 -283
- package/dist/node/node-compression.d.ts +0 -2
- package/dist/node/node-compression.d.ts.map +0 -1
- package/dist/node/node-compression.js +0 -1
- package/dist/node/node-request.d.ts +0 -2
- package/dist/node/node-request.d.ts.map +0 -1
- package/dist/node/node-request.js +0 -1
- package/dist/node/node-response.d.ts +0 -2
- package/dist/node/node-response.d.ts.map +0 -1
- package/dist/node/node-response.js +0 -1
- package/dist/node/node-shutdown.d.ts +0 -2
- package/dist/node/node-shutdown.d.ts.map +0 -1
- package/dist/node/node-shutdown.js +0 -1
- package/dist/node/node.d.ts +0 -2
- package/dist/node/node.d.ts.map +0 -1
- package/dist/node/node.js +0 -1
- package/dist/node.d.ts +0 -5
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js +0 -3
package/dist/multipart.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { PayloadTooLargeException } from '@fluojs/http';
|
|
1
|
+
import { PayloadTooLargeException } from '@fluojs/http/portable';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Represents a single uploaded multipart file buffered in memory.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Configures multipart parsing limits for
|
|
8
|
+
* Configures multipart parsing limits for fields, files, headers, and total payload size.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -16,53 +16,62 @@ import { PayloadTooLargeException } from '@fluojs/http';
|
|
|
16
16
|
* Describes request-like multipart inputs accepted by the runtime parsers.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A decoded non-file multipart field.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A multipart file whose bytes are read exactly once from a portable Web stream.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A typed item yielded by {@link parseMultipartStream}.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Signals that a multipart body has already been selected for buffered or streaming consumption.
|
|
33
|
+
*/
|
|
34
|
+
export class MultipartBodyConsumedError extends Error {
|
|
35
|
+
constructor() {
|
|
36
|
+
super('Multipart request body has already been consumed.');
|
|
37
|
+
this.name = 'MultipartBodyConsumedError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
19
40
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
41
|
+
const DEFAULT_MAX_FIELD_SIZE = 1 * 1024 * 1024;
|
|
42
|
+
const DEFAULT_MAX_FIELDS = 100;
|
|
20
43
|
const DEFAULT_MAX_FILES = 10;
|
|
44
|
+
const DEFAULT_MAX_HEADER_SIZE = 8 * 1024;
|
|
21
45
|
const DEFAULT_MAX_TOTAL_SIZE = 10 * 1024 * 1024;
|
|
22
46
|
const MULTIPART_BODY_LIMIT_MESSAGE = 'Multipart body exceeds the maximum size of';
|
|
23
47
|
const TEXT_ENCODER = new TextEncoder();
|
|
48
|
+
const TEXT_DECODER = new TextDecoder();
|
|
49
|
+
const CONSUMED_MULTIPART_INPUTS = new WeakSet();
|
|
24
50
|
|
|
25
51
|
/**
|
|
26
52
|
* Parses a multipart request into string fields and in-memory uploaded files.
|
|
27
53
|
*
|
|
28
54
|
* @param request - Web `Request` or request-like input carrying a multipart body.
|
|
29
|
-
* @param options - Multipart limits for file size, file count,
|
|
55
|
+
* @param options - Multipart limits for file size, file count, total size, field count, field size, and header size.
|
|
30
56
|
* @returns Parsed string fields plus uploaded files buffered in memory.
|
|
31
|
-
* @throws {PayloadTooLargeException} When
|
|
57
|
+
* @throws {PayloadTooLargeException} When any configured file-size, file-count, total-size, field-count, field-size,
|
|
58
|
+
* or header-size limit is exceeded.
|
|
32
59
|
*/
|
|
33
60
|
export async function parseMultipart(request, options = {}) {
|
|
34
|
-
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
35
|
-
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
36
|
-
const maxTotalSize = options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE;
|
|
37
|
-
const formData = await toWebRequest(request, maxTotalSize).formData();
|
|
38
61
|
const fields = {};
|
|
39
62
|
const files = [];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
totalSize += TEXT_ENCODER.encode(value).byteLength;
|
|
44
|
-
if (totalSize > maxTotalSize) {
|
|
45
|
-
throw new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
|
|
46
|
-
}
|
|
47
|
-
appendMultipartField(fields, fieldname, value);
|
|
63
|
+
for await (const part of createMultipartParts(request, options, false)) {
|
|
64
|
+
if (part.kind === 'field') {
|
|
65
|
+
appendMultipartField(fields, part.name, part.value);
|
|
48
66
|
continue;
|
|
49
67
|
}
|
|
50
|
-
|
|
51
|
-
if (totalSize > maxTotalSize) {
|
|
52
|
-
throw new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
|
|
53
|
-
}
|
|
54
|
-
if (files.length >= maxFiles) {
|
|
55
|
-
throw new PayloadTooLargeException(`Exceeded maximum file count of ${String(maxFiles)}.`);
|
|
56
|
-
}
|
|
57
|
-
if (value.size > maxFileSize) {
|
|
58
|
-
throw new PayloadTooLargeException(`File "${fieldname}" exceeds the maximum size of ${String(maxFileSize)} bytes.`);
|
|
59
|
-
}
|
|
68
|
+
const buffer = new Uint8Array(await new Response(part.stream).arrayBuffer());
|
|
60
69
|
files.push({
|
|
61
|
-
buffer
|
|
62
|
-
fieldname,
|
|
63
|
-
mimetype:
|
|
64
|
-
originalname:
|
|
65
|
-
size:
|
|
70
|
+
buffer,
|
|
71
|
+
fieldname: part.name,
|
|
72
|
+
mimetype: part.contentType,
|
|
73
|
+
originalname: part.filename,
|
|
74
|
+
size: buffer.byteLength
|
|
66
75
|
});
|
|
67
76
|
}
|
|
68
77
|
return {
|
|
@@ -70,110 +79,663 @@ export async function parseMultipart(request, options = {}) {
|
|
|
70
79
|
files
|
|
71
80
|
};
|
|
72
81
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Streams multipart fields and file parts from a request body.
|
|
85
|
+
*
|
|
86
|
+
* A yielded file stream must finish or be cancelled before requesting the next part. Calling
|
|
87
|
+
* this function selects the request body for streaming consumption; callers cannot also use
|
|
88
|
+
* {@link parseMultipart} for the same request body.
|
|
89
|
+
*
|
|
90
|
+
* @param request - Web `Request` or request-like input carrying a multipart body.
|
|
91
|
+
* @param options - Limits enforced while parsing headers, fields, files, and total input bytes.
|
|
92
|
+
* @returns An async iterator of typed field and file parts.
|
|
93
|
+
* @throws {MultipartBodyConsumedError} When buffered or streaming parsing already selected the body.
|
|
94
|
+
*/
|
|
95
|
+
export function parseMultipartStream(request, options = {}) {
|
|
96
|
+
return createMultipartParts(request, options, true);
|
|
97
|
+
}
|
|
98
|
+
function createMultipartParts(request, options, useStreamingDefaults) {
|
|
99
|
+
markMultipartBodyConsumed(request);
|
|
100
|
+
return new MultipartPartIterator(new MultipartStreamParser(request, options, useStreamingDefaults));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Marks an input as selected for buffered or streaming multipart consumption.
|
|
105
|
+
*
|
|
106
|
+
* @param request - Request input whose body cannot be selected again.
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export function markMultipartBodyConsumed(request) {
|
|
110
|
+
const body = request instanceof Request ? request.body : request.body;
|
|
111
|
+
const inputs = [request];
|
|
112
|
+
if (body !== null && typeof body === 'object') {
|
|
113
|
+
inputs.push(body);
|
|
78
114
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
if (request instanceof Request && request.bodyUsed || inputs.some(input => CONSUMED_MULTIPART_INPUTS.has(input))) {
|
|
116
|
+
throw new MultipartBodyConsumedError();
|
|
117
|
+
}
|
|
118
|
+
for (const input of inputs) {
|
|
119
|
+
CONSUMED_MULTIPART_INPUTS.add(input);
|
|
82
120
|
}
|
|
83
|
-
fields[name] = [existing, value];
|
|
84
121
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
class MultipartPartIterator {
|
|
123
|
+
closed = false;
|
|
124
|
+
constructor(parser) {
|
|
125
|
+
this.parser = parser;
|
|
126
|
+
}
|
|
127
|
+
[Symbol.asyncIterator]() {
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
async next() {
|
|
131
|
+
if (this.closed) {
|
|
132
|
+
return {
|
|
133
|
+
done: true,
|
|
134
|
+
value: undefined
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const part = await this.parser.nextPart();
|
|
138
|
+
if (part === undefined) {
|
|
139
|
+
this.closed = true;
|
|
140
|
+
return {
|
|
141
|
+
done: true,
|
|
142
|
+
value: undefined
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
done: false,
|
|
147
|
+
value: part
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async return() {
|
|
151
|
+
this.closed = true;
|
|
152
|
+
this.parser.closeIfIncomplete();
|
|
153
|
+
return {
|
|
154
|
+
done: true,
|
|
155
|
+
value: undefined
|
|
156
|
+
};
|
|
88
157
|
}
|
|
89
|
-
const method = request.method ?? 'POST';
|
|
90
|
-
const body = supportsRequestBody(method) ? resolveRequestBody(request) : undefined;
|
|
91
|
-
return createMultipartRequest(request.url ?? 'http://localhost/', method, normalizeRequestHeaders(request.headers), body, maxTotalSize);
|
|
92
158
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
159
|
+
class MultipartStreamParser {
|
|
160
|
+
activeFile = false;
|
|
161
|
+
buffer = new Uint8Array();
|
|
162
|
+
cancellationStarted = false;
|
|
163
|
+
done = false;
|
|
164
|
+
failure;
|
|
165
|
+
fileController;
|
|
166
|
+
fileDelimiter;
|
|
167
|
+
initialDelimiter;
|
|
168
|
+
initialized = false;
|
|
169
|
+
maxFieldSize;
|
|
170
|
+
maxFields;
|
|
171
|
+
maxFileSize;
|
|
172
|
+
maxFiles;
|
|
173
|
+
maxHeaderSize;
|
|
174
|
+
maxTotalSize;
|
|
175
|
+
reader;
|
|
176
|
+
readerReadPending = false;
|
|
177
|
+
released = false;
|
|
178
|
+
signal;
|
|
179
|
+
terminalDrained = false;
|
|
180
|
+
totalSize = 0;
|
|
181
|
+
fieldCount = 0;
|
|
182
|
+
fileCount = 0;
|
|
183
|
+
partEnded = false;
|
|
184
|
+
constructor(request, options, useStreamingDefaults) {
|
|
185
|
+
const headers = normalizeRequestHeaders(request.headers);
|
|
186
|
+
const boundary = extractMultipartBoundary(headers.get('content-type'));
|
|
187
|
+
this.initialDelimiter = TEXT_ENCODER.encode(`--${boundary}`);
|
|
188
|
+
this.fileDelimiter = TEXT_ENCODER.encode(`\r\n--${boundary}`);
|
|
189
|
+
this.maxTotalSize = options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE;
|
|
190
|
+
this.maxFieldSize = options.maxFieldSize ?? (useStreamingDefaults ? DEFAULT_MAX_FIELD_SIZE : this.maxTotalSize);
|
|
191
|
+
this.maxFields = options.maxFields ?? (useStreamingDefaults ? DEFAULT_MAX_FIELDS : Number.MAX_SAFE_INTEGER);
|
|
192
|
+
this.maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
193
|
+
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
194
|
+
this.maxHeaderSize = options.maxHeaderSize ?? (useStreamingDefaults ? DEFAULT_MAX_HEADER_SIZE : this.maxTotalSize);
|
|
195
|
+
this.signal = request instanceof Request ? request.signal : request.signal;
|
|
196
|
+
this.reader = resolveMultipartStreamBody(request).getReader();
|
|
197
|
+
this.signal?.addEventListener('abort', this.handleAbort, {
|
|
198
|
+
once: true
|
|
199
|
+
});
|
|
200
|
+
if (this.signal?.aborted) {
|
|
201
|
+
this.handleAbort();
|
|
202
|
+
}
|
|
203
|
+
const contentLengthError = getContentLengthError(headers, this.maxTotalSize);
|
|
204
|
+
if (contentLengthError) {
|
|
205
|
+
this.fail(contentLengthError);
|
|
206
|
+
} else if (this.signal?.aborted) {
|
|
207
|
+
this.fail(toAbortError(this.signal.reason));
|
|
208
|
+
}
|
|
96
209
|
}
|
|
97
|
-
|
|
98
|
-
|
|
210
|
+
async nextPart() {
|
|
211
|
+
this.throwIfFailed();
|
|
212
|
+
if (this.activeFile) {
|
|
213
|
+
throw new Error('Consume or cancel the active multipart file stream before reading the next part.');
|
|
214
|
+
}
|
|
215
|
+
if (!this.initialized) {
|
|
216
|
+
await this.consumeInitialDelimiter();
|
|
217
|
+
this.initialized = true;
|
|
218
|
+
}
|
|
219
|
+
if (this.done) {
|
|
220
|
+
await this.drainAfterClosingBoundary();
|
|
221
|
+
this.release();
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const headers = await this.readHeaders();
|
|
225
|
+
let disposition;
|
|
226
|
+
try {
|
|
227
|
+
disposition = parseContentDisposition(headers['content-disposition']);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.fail(error);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
if (disposition.filename === undefined) {
|
|
233
|
+
this.fieldCount += 1;
|
|
234
|
+
if (this.fieldCount > this.maxFields) {
|
|
235
|
+
this.fail(new PayloadTooLargeException(`Exceeded maximum field count of ${String(this.maxFields)}.`));
|
|
236
|
+
this.throwIfFailed();
|
|
237
|
+
}
|
|
238
|
+
const value = await this.readFieldValue();
|
|
239
|
+
return {
|
|
240
|
+
headers,
|
|
241
|
+
kind: 'field',
|
|
242
|
+
name: disposition.name,
|
|
243
|
+
value
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
this.fileCount += 1;
|
|
247
|
+
if (this.fileCount > this.maxFiles) {
|
|
248
|
+
this.fail(new PayloadTooLargeException(`Exceeded maximum file count of ${String(this.maxFiles)}.`));
|
|
249
|
+
this.throwIfFailed();
|
|
250
|
+
}
|
|
251
|
+
this.activeFile = true;
|
|
252
|
+
return {
|
|
253
|
+
contentType: headers['content-type'] ?? 'application/octet-stream',
|
|
254
|
+
filename: disposition.filename,
|
|
255
|
+
headers,
|
|
256
|
+
kind: 'file',
|
|
257
|
+
name: disposition.name,
|
|
258
|
+
stream: this.createFileStream(disposition.name)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
closeIfIncomplete() {
|
|
262
|
+
if (!this.failure && (this.activeFile || !this.terminalDrained)) {
|
|
263
|
+
this.fail(new Error('Multipart parser has been cancelled.'));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
handleAbort = () => {
|
|
267
|
+
this.fail(toAbortError(this.signal?.reason));
|
|
268
|
+
};
|
|
269
|
+
createFileStream(name) {
|
|
270
|
+
let size = 0;
|
|
271
|
+
return new ReadableStream({
|
|
272
|
+
cancel: async reason => {
|
|
273
|
+
this.fail(reason);
|
|
274
|
+
},
|
|
275
|
+
pull: async controller => {
|
|
276
|
+
this.fileController = controller;
|
|
277
|
+
try {
|
|
278
|
+
const chunk = await this.readPartChunk();
|
|
279
|
+
if (chunk === undefined) {
|
|
280
|
+
this.activeFile = false;
|
|
281
|
+
this.fileController = undefined;
|
|
282
|
+
controller.close();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
size += chunk.byteLength;
|
|
286
|
+
if (size > this.maxFileSize) {
|
|
287
|
+
const error = new PayloadTooLargeException(`File "${name}" exceeds the maximum size of ${String(this.maxFileSize)} bytes.`);
|
|
288
|
+
this.fail(error);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
controller.enqueue(chunk);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const parserError = toError(error);
|
|
294
|
+
this.activeFile = false;
|
|
295
|
+
this.fileController = undefined;
|
|
296
|
+
controller.error(parserError);
|
|
297
|
+
throw parserError;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}, {
|
|
301
|
+
highWaterMark: 0
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async consumeInitialDelimiter() {
|
|
305
|
+
await this.ensure(this.initialDelimiter.byteLength + 2);
|
|
306
|
+
if (!matchesAt(this.buffer, this.initialDelimiter, 0)) {
|
|
307
|
+
this.fail(new Error('Multipart body does not start with its declared boundary.'));
|
|
308
|
+
this.throwIfFailed();
|
|
309
|
+
}
|
|
310
|
+
this.consume(this.initialDelimiter.byteLength);
|
|
311
|
+
const ending = this.consume(2);
|
|
312
|
+
if (equalsBytes(ending, TEXT_ENCODER.encode('--'))) {
|
|
313
|
+
this.done = true;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!equalsBytes(ending, TEXT_ENCODER.encode('\r\n'))) {
|
|
317
|
+
this.fail(new Error('Multipart boundary is malformed.'));
|
|
318
|
+
this.throwIfFailed();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async readHeaders() {
|
|
322
|
+
const headerBytes = await this.readUntil(TEXT_ENCODER.encode('\r\n\r\n'), this.maxHeaderSize, `Multipart part headers exceed the maximum size of ${String(this.maxHeaderSize)} bytes.`);
|
|
323
|
+
const headers = {};
|
|
324
|
+
for (const line of TEXT_DECODER.decode(headerBytes).split('\r\n')) {
|
|
325
|
+
const separator = line.indexOf(':');
|
|
326
|
+
if (separator <= 0) {
|
|
327
|
+
this.fail(new Error('Multipart part contains a malformed header.'));
|
|
328
|
+
this.throwIfFailed();
|
|
329
|
+
}
|
|
330
|
+
const name = line.slice(0, separator).trim().toLowerCase();
|
|
331
|
+
const value = line.slice(separator + 1).trim();
|
|
332
|
+
if (headers[name] !== undefined) {
|
|
333
|
+
this.fail(new Error(`Multipart part contains a repeated "${name}" header.`));
|
|
334
|
+
this.throwIfFailed();
|
|
335
|
+
}
|
|
336
|
+
headers[name] = value;
|
|
337
|
+
}
|
|
338
|
+
return Object.freeze(headers);
|
|
339
|
+
}
|
|
340
|
+
async readFieldValue() {
|
|
341
|
+
const chunks = [];
|
|
342
|
+
let size = 0;
|
|
343
|
+
while (true) {
|
|
344
|
+
const chunk = await this.readPartChunk();
|
|
345
|
+
if (chunk === undefined) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
size += chunk.byteLength;
|
|
349
|
+
if (size > this.maxFieldSize) {
|
|
350
|
+
this.fail(new PayloadTooLargeException(`Field exceeds the maximum size of ${String(this.maxFieldSize)} bytes.`));
|
|
351
|
+
this.throwIfFailed();
|
|
352
|
+
}
|
|
353
|
+
chunks.push(chunk);
|
|
354
|
+
}
|
|
355
|
+
return TEXT_DECODER.decode(concatBytes(chunks, size));
|
|
356
|
+
}
|
|
357
|
+
async readPartChunk() {
|
|
358
|
+
if (this.partEnded) {
|
|
359
|
+
this.partEnded = false;
|
|
360
|
+
if (this.done) {
|
|
361
|
+
await this.drainAfterClosingBoundary();
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
while (true) {
|
|
366
|
+
this.throwIfFailed();
|
|
367
|
+
const delimiterIndex = indexOfBytes(this.buffer, this.fileDelimiter);
|
|
368
|
+
if (delimiterIndex !== -1) {
|
|
369
|
+
const suffixOffset = delimiterIndex + this.fileDelimiter.byteLength;
|
|
370
|
+
const suffixLength = 2;
|
|
371
|
+
if (this.buffer.byteLength < suffixOffset + suffixLength) {
|
|
372
|
+
if (delimiterIndex > 0) {
|
|
373
|
+
return this.consume(delimiterIndex);
|
|
374
|
+
}
|
|
375
|
+
await this.readMore();
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const ending = this.buffer.slice(suffixOffset, suffixOffset + suffixLength);
|
|
379
|
+
if (equalsBytes(ending, TEXT_ENCODER.encode('--'))) {
|
|
380
|
+
const chunk = this.consume(delimiterIndex);
|
|
381
|
+
this.consume(this.fileDelimiter.byteLength + suffixLength);
|
|
382
|
+
this.done = true;
|
|
383
|
+
this.partEnded = chunk.byteLength > 0;
|
|
384
|
+
if (chunk.byteLength === 0) {
|
|
385
|
+
await this.drainAfterClosingBoundary();
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
return chunk;
|
|
389
|
+
} else if (equalsBytes(ending, TEXT_ENCODER.encode('\r\n'))) {
|
|
390
|
+
const chunk = this.consume(delimiterIndex);
|
|
391
|
+
this.consume(this.fileDelimiter.byteLength + suffixLength);
|
|
392
|
+
this.partEnded = chunk.byteLength > 0;
|
|
393
|
+
return chunk.byteLength === 0 ? undefined : chunk;
|
|
394
|
+
}
|
|
395
|
+
return this.consume(suffixOffset);
|
|
396
|
+
}
|
|
397
|
+
const retainedBytes = this.fileDelimiter.byteLength - 1;
|
|
398
|
+
if (this.buffer.byteLength > retainedBytes) {
|
|
399
|
+
return this.consume(this.buffer.byteLength - retainedBytes);
|
|
400
|
+
}
|
|
401
|
+
await this.readMore();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async readUntil(delimiter, limit, message) {
|
|
405
|
+
while (true) {
|
|
406
|
+
this.throwIfFailed();
|
|
407
|
+
const delimiterIndex = indexOfBytes(this.buffer, delimiter);
|
|
408
|
+
if (delimiterIndex !== -1) {
|
|
409
|
+
const value = this.consume(delimiterIndex);
|
|
410
|
+
this.consume(delimiter.byteLength);
|
|
411
|
+
if (value.byteLength > limit) {
|
|
412
|
+
this.fail(new PayloadTooLargeException(message));
|
|
413
|
+
this.throwIfFailed();
|
|
414
|
+
}
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
const confirmedSize = this.buffer.byteLength - trailingDelimiterPrefixLength(this.buffer, delimiter);
|
|
418
|
+
if (confirmedSize > limit) {
|
|
419
|
+
this.fail(new PayloadTooLargeException(message));
|
|
420
|
+
this.throwIfFailed();
|
|
421
|
+
}
|
|
422
|
+
await this.readMore();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async ensure(size) {
|
|
426
|
+
while (this.buffer.byteLength < size) {
|
|
427
|
+
await this.readMore();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async readMore(allowEof = false) {
|
|
431
|
+
this.throwIfFailed();
|
|
432
|
+
this.readerReadPending = true;
|
|
433
|
+
let result;
|
|
434
|
+
try {
|
|
435
|
+
result = await this.reader.read();
|
|
436
|
+
} catch (error) {
|
|
437
|
+
this.fail(error);
|
|
438
|
+
throw error;
|
|
439
|
+
} finally {
|
|
440
|
+
this.readerReadPending = false;
|
|
441
|
+
if (this.failure) {
|
|
442
|
+
this.release();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
this.throwIfFailed();
|
|
446
|
+
if (result.done) {
|
|
447
|
+
if (allowEof) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const error = new Error('Multipart body ended before its closing boundary.');
|
|
451
|
+
this.fail(error);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
const chunk = result.value;
|
|
455
|
+
this.totalSize += chunk.byteLength;
|
|
456
|
+
if (this.totalSize > this.maxTotalSize) {
|
|
457
|
+
this.fail(new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(this.maxTotalSize)} bytes.`));
|
|
458
|
+
this.throwIfFailed();
|
|
459
|
+
}
|
|
460
|
+
this.buffer = concatBytes([this.buffer, chunk], this.buffer.byteLength + chunk.byteLength);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
consume(size) {
|
|
464
|
+
const value = this.buffer.slice(0, size);
|
|
465
|
+
this.buffer = this.buffer.slice(size);
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
async drainAfterClosingBoundary() {
|
|
469
|
+
if (this.terminalDrained) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
while (true) {
|
|
473
|
+
this.buffer = new Uint8Array();
|
|
474
|
+
if (!(await this.readMore(true))) {
|
|
475
|
+
this.terminalDrained = true;
|
|
476
|
+
this.release();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
fail(reason) {
|
|
482
|
+
if (this.failure) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const error = toError(reason);
|
|
486
|
+
this.failure = error;
|
|
487
|
+
this.done = true;
|
|
488
|
+
this.fileController?.error(error);
|
|
489
|
+
this.fileController = undefined;
|
|
490
|
+
this.activeFile = false;
|
|
491
|
+
this.signal?.removeEventListener('abort', this.handleAbort);
|
|
492
|
+
if (!this.cancellationStarted) {
|
|
493
|
+
this.cancellationStarted = true;
|
|
494
|
+
void this.reader.cancel(reason).then(() => this.release(), () => this.release());
|
|
495
|
+
}
|
|
496
|
+
this.release();
|
|
497
|
+
}
|
|
498
|
+
release() {
|
|
499
|
+
if (this.released || this.readerReadPending) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.released = true;
|
|
503
|
+
this.signal?.removeEventListener('abort', this.handleAbort);
|
|
504
|
+
this.reader.releaseLock();
|
|
505
|
+
}
|
|
506
|
+
throwIfFailed() {
|
|
507
|
+
if (this.failure) {
|
|
508
|
+
throw this.failure;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function extractMultipartBoundary(contentType) {
|
|
513
|
+
const match = contentType?.match(/(?:^|;)\s*boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
514
|
+
const boundary = match?.[1] ?? match?.[2]?.trim();
|
|
515
|
+
if (!boundary) {
|
|
516
|
+
throw new Error('Multipart requests require a boundary parameter.');
|
|
517
|
+
}
|
|
518
|
+
return boundary;
|
|
519
|
+
}
|
|
520
|
+
function parseContentDisposition(value) {
|
|
521
|
+
if (value === undefined) {
|
|
522
|
+
throw new Error('Multipart parts require a form-data Content-Disposition header.');
|
|
523
|
+
}
|
|
524
|
+
let index = skipWhitespace(value, 0);
|
|
525
|
+
const dispositionType = readToken(value, index);
|
|
526
|
+
if (!dispositionType || dispositionType.value.toLowerCase() !== 'form-data') {
|
|
527
|
+
throw new Error('Multipart parts require a form-data Content-Disposition header.');
|
|
528
|
+
}
|
|
529
|
+
index = dispositionType.end;
|
|
530
|
+
const parameters = new Map();
|
|
531
|
+
while (index < value.length) {
|
|
532
|
+
index = skipWhitespace(value, index);
|
|
533
|
+
if (index === value.length) {
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
if (value[index] !== ';') {
|
|
537
|
+
throw new Error('Multipart Content-Disposition parameters must be separated by semicolons.');
|
|
538
|
+
}
|
|
539
|
+
index = skipWhitespace(value, index + 1);
|
|
540
|
+
const parameterName = readToken(value, index);
|
|
541
|
+
if (!parameterName) {
|
|
542
|
+
throw new Error('Multipart Content-Disposition parameters require token names.');
|
|
543
|
+
}
|
|
544
|
+
index = skipWhitespace(value, parameterName.end);
|
|
545
|
+
if (value[index] !== '=') {
|
|
546
|
+
throw new Error('Multipart Content-Disposition parameters require values.');
|
|
547
|
+
}
|
|
548
|
+
index = skipWhitespace(value, index + 1);
|
|
549
|
+
const parameterValue = value[index] === '"' ? readQuotedString(value, index) : readToken(value, index);
|
|
550
|
+
if (!parameterValue) {
|
|
551
|
+
throw new Error('Multipart Content-Disposition parameters require token or quoted-string values.');
|
|
552
|
+
}
|
|
553
|
+
if (!parameters.has(parameterName.value.toLowerCase())) {
|
|
554
|
+
parameters.set(parameterName.value.toLowerCase(), parameterValue.value);
|
|
555
|
+
}
|
|
556
|
+
index = parameterValue.end;
|
|
557
|
+
}
|
|
558
|
+
const name = parameters.get('name');
|
|
559
|
+
if (name === undefined) {
|
|
560
|
+
throw new Error('Multipart parts require a name parameter.');
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
filename: parameters.get('filename'),
|
|
564
|
+
name
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function readQuotedString(value, start) {
|
|
568
|
+
let index = start + 1;
|
|
569
|
+
let result = '';
|
|
570
|
+
while (index < value.length) {
|
|
571
|
+
const character = value[index];
|
|
572
|
+
if (character === '\r' || character === '\n') {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
if (character === '"') {
|
|
576
|
+
return {
|
|
577
|
+
end: index + 1,
|
|
578
|
+
value: result
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (character === '\\') {
|
|
582
|
+
index += 1;
|
|
583
|
+
const escaped = value[index];
|
|
584
|
+
if (escaped === undefined || escaped === '\r' || escaped === '\n') {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
result += escaped;
|
|
588
|
+
index += 1;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
result += character;
|
|
592
|
+
index += 1;
|
|
99
593
|
}
|
|
100
594
|
return undefined;
|
|
101
595
|
}
|
|
102
|
-
function
|
|
103
|
-
|
|
596
|
+
function readToken(value, start) {
|
|
597
|
+
let end = start;
|
|
598
|
+
while (end < value.length && isTokenCharacter(value[end])) {
|
|
599
|
+
end += 1;
|
|
600
|
+
}
|
|
601
|
+
return end === start ? undefined : {
|
|
602
|
+
end,
|
|
603
|
+
value: value.slice(start, end)
|
|
604
|
+
};
|
|
104
605
|
}
|
|
105
|
-
function
|
|
106
|
-
return
|
|
606
|
+
function isTokenCharacter(character) {
|
|
607
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]$/.test(character);
|
|
107
608
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
609
|
+
function skipWhitespace(value, start) {
|
|
610
|
+
let index = start;
|
|
611
|
+
while (value[index] === ' ' || value[index] === '\t') {
|
|
612
|
+
index += 1;
|
|
613
|
+
}
|
|
614
|
+
return index;
|
|
110
615
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await iterator.return?.();
|
|
116
|
-
},
|
|
117
|
-
async pull(controller) {
|
|
118
|
-
const {
|
|
119
|
-
done,
|
|
120
|
-
value
|
|
121
|
-
} = await iterator.next();
|
|
122
|
-
if (done) {
|
|
123
|
-
controller.close();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
controller.enqueue(value);
|
|
616
|
+
function resolveMultipartStreamBody(request) {
|
|
617
|
+
if (request instanceof Request) {
|
|
618
|
+
if (!request.body) {
|
|
619
|
+
throw new Error('Multipart requests require a body.');
|
|
127
620
|
}
|
|
128
|
-
|
|
621
|
+
return request.body;
|
|
622
|
+
}
|
|
623
|
+
const body = request.body ?? (typeof request[Symbol.asyncIterator] === 'function' ? request : undefined);
|
|
624
|
+
if (body instanceof ReadableStream) {
|
|
625
|
+
return body;
|
|
626
|
+
}
|
|
627
|
+
if (body && isAsyncIterableBody(body)) {
|
|
628
|
+
return createReadableStreamFromAsyncIterable(body);
|
|
629
|
+
}
|
|
630
|
+
if (body === undefined || body === null) {
|
|
631
|
+
throw new Error('Multipart requests require a body.');
|
|
632
|
+
}
|
|
633
|
+
const converted = new Request(request.url ?? 'http://localhost/', {
|
|
634
|
+
body: body,
|
|
635
|
+
method: request.method ?? 'POST'
|
|
636
|
+
}).body;
|
|
637
|
+
if (!converted) {
|
|
638
|
+
throw new Error('Multipart requests require a body.');
|
|
639
|
+
}
|
|
640
|
+
return converted;
|
|
129
641
|
}
|
|
130
|
-
function
|
|
642
|
+
function getContentLengthError(headers, maxTotalSize) {
|
|
131
643
|
const contentLength = headers.get('content-length');
|
|
132
|
-
if (contentLength
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
644
|
+
if (contentLength === null) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
const parsedContentLength = Number(contentLength);
|
|
648
|
+
if (Number.isFinite(parsedContentLength) && parsedContentLength > maxTotalSize) {
|
|
649
|
+
return new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
function indexOfBytes(input, needle) {
|
|
654
|
+
if (needle.byteLength === 0 || needle.byteLength > input.byteLength) {
|
|
655
|
+
return -1;
|
|
656
|
+
}
|
|
657
|
+
outer: for (let index = 0; index <= input.byteLength - needle.byteLength; index += 1) {
|
|
658
|
+
for (let offset = 0; offset < needle.byteLength; offset += 1) {
|
|
659
|
+
if (input[index + offset] !== needle[offset]) {
|
|
660
|
+
continue outer;
|
|
661
|
+
}
|
|
136
662
|
}
|
|
663
|
+
return index;
|
|
137
664
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
665
|
+
return -1;
|
|
666
|
+
}
|
|
667
|
+
function matchesAt(input, needle, start) {
|
|
668
|
+
if (start + needle.byteLength > input.byteLength) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
for (let offset = 0; offset < needle.byteLength; offset += 1) {
|
|
672
|
+
if (input[start + offset] !== needle[offset]) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
function equalsBytes(left, right) {
|
|
679
|
+
return left.byteLength === right.byteLength && matchesAt(left, right, 0);
|
|
680
|
+
}
|
|
681
|
+
function concatBytes(chunks, size) {
|
|
682
|
+
const result = new Uint8Array(size);
|
|
683
|
+
let offset = 0;
|
|
684
|
+
for (const chunk of chunks) {
|
|
685
|
+
result.set(chunk, offset);
|
|
686
|
+
offset += chunk.byteLength;
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
function trailingDelimiterPrefixLength(buffer, delimiter) {
|
|
691
|
+
const maximum = Math.min(buffer.byteLength, delimiter.byteLength - 1);
|
|
692
|
+
for (let size = maximum; size > 0; size -= 1) {
|
|
693
|
+
if (equalsBytes(buffer.slice(buffer.byteLength - size), delimiter.slice(0, size))) {
|
|
694
|
+
return size;
|
|
147
695
|
}
|
|
148
696
|
}
|
|
149
|
-
return
|
|
697
|
+
return 0;
|
|
150
698
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
699
|
+
function toAbortError(reason) {
|
|
700
|
+
return reason instanceof Error ? reason : new Error('Multipart request was aborted.');
|
|
701
|
+
}
|
|
702
|
+
function toError(error) {
|
|
703
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
704
|
+
}
|
|
705
|
+
function appendMultipartField(fields, name, value) {
|
|
706
|
+
const existing = fields[name];
|
|
707
|
+
if (existing === undefined) {
|
|
708
|
+
fields[name] = value;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (Array.isArray(existing)) {
|
|
712
|
+
existing.push(value);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
fields[name] = [existing, value];
|
|
716
|
+
}
|
|
717
|
+
function isAsyncIterableBody(body) {
|
|
718
|
+
return body !== null && typeof body === 'object' && Symbol.asyncIterator in body;
|
|
719
|
+
}
|
|
720
|
+
function createReadableStreamFromAsyncIterable(source) {
|
|
721
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
155
722
|
return new ReadableStream({
|
|
156
|
-
async cancel(
|
|
157
|
-
await
|
|
723
|
+
async cancel() {
|
|
724
|
+
await iterator.return?.();
|
|
158
725
|
},
|
|
159
726
|
async pull(controller) {
|
|
160
727
|
const {
|
|
161
728
|
done,
|
|
162
729
|
value
|
|
163
|
-
} = await
|
|
730
|
+
} = await iterator.next();
|
|
164
731
|
if (done) {
|
|
165
732
|
controller.close();
|
|
166
733
|
return;
|
|
167
734
|
}
|
|
168
|
-
totalSize += value.byteLength;
|
|
169
|
-
if (totalSize > maxTotalSize) {
|
|
170
|
-
const error = new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
|
|
171
|
-
await reader.cancel(error.message);
|
|
172
|
-
controller.error(error);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
735
|
controller.enqueue(value);
|
|
176
736
|
}
|
|
737
|
+
}, {
|
|
738
|
+
highWaterMark: 0
|
|
177
739
|
});
|
|
178
740
|
}
|
|
179
741
|
function normalizeRequestHeaders(headers) {
|