@ccheever/exact-ibex-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- package/src/window/index.ts +872 -0
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Body Handling Utilities
|
|
4
|
+
*
|
|
5
|
+
* Implements body mixin functionality for Request and Response.
|
|
6
|
+
* @see https://fetch.spec.whatwg.org/#body-mixin
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { BodyInit, BufferSource } from './types.js';
|
|
10
|
+
import { BodyConsumedError } from './errors.js';
|
|
11
|
+
import { isReadableStream, ReadableStream as RuntimeReadableStream } from '../streams/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Text encoder/decoder - lazy initialization with fallbacks for Hermes.
|
|
15
|
+
*/
|
|
16
|
+
let _textEncoder: { encode(s: string): Uint8Array } | null = null;
|
|
17
|
+
let _textDecoder: { decode(b: BufferSource): string } | null = null;
|
|
18
|
+
let _textDecoderNoBOM: { decode(b: BufferSource): string } | null = null;
|
|
19
|
+
|
|
20
|
+
function isBunCompatFetchTest(): boolean {
|
|
21
|
+
if ((globalThis as { __exactRuntimeContext?: string }).__exactRuntimeContext === 'shell') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (typeof process !== 'object' || !process || typeof process.env !== 'object') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return process.env.EXACT_COMPAT_TEST === '1' && process.env.EXACT_TEST_SECTION === 'bun';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeBlobContentType(contentType: string | null): string {
|
|
32
|
+
if (!contentType) {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalized = contentType
|
|
37
|
+
.split(';')
|
|
38
|
+
.map((part) => part.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(';')
|
|
41
|
+
.toLowerCase();
|
|
42
|
+
|
|
43
|
+
if (!isBunCompatFetchTest() || normalized.includes('charset=')) {
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mediaType = normalized.split(';', 1)[0];
|
|
48
|
+
const isUtf8TextLike =
|
|
49
|
+
mediaType.startsWith('text/') ||
|
|
50
|
+
mediaType === 'application/json' ||
|
|
51
|
+
mediaType === 'application/javascript' ||
|
|
52
|
+
mediaType === 'application/x-www-form-urlencoded' ||
|
|
53
|
+
mediaType === 'application/xml' ||
|
|
54
|
+
mediaType.endsWith('+json') ||
|
|
55
|
+
mediaType.endsWith('+xml');
|
|
56
|
+
|
|
57
|
+
return isUtf8TextLike ? `${normalized};charset=utf-8` : normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getTextEncoder(): { encode(s: string): Uint8Array } {
|
|
61
|
+
if (_textEncoder) return _textEncoder;
|
|
62
|
+
|
|
63
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
64
|
+
_textEncoder = new TextEncoder();
|
|
65
|
+
} else {
|
|
66
|
+
// UTF-8 fallback for Hermes (handles full Unicode including surrogate pairs)
|
|
67
|
+
_textEncoder = {
|
|
68
|
+
encode(str: string): Uint8Array {
|
|
69
|
+
const bytes = new Uint8Array(str.length * 4); // Max 4 bytes per code unit
|
|
70
|
+
let offset = 0;
|
|
71
|
+
for (let i = 0; i < str.length; i++) {
|
|
72
|
+
const code = str.charCodeAt(i);
|
|
73
|
+
if (code < 0x80) {
|
|
74
|
+
bytes[offset++] = code;
|
|
75
|
+
} else if (code < 0x800) {
|
|
76
|
+
bytes[offset++] = 0xc0 | (code >> 6);
|
|
77
|
+
bytes[offset++] = 0x80 | (code & 0x3f);
|
|
78
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
79
|
+
// High surrogate: combine with next low surrogate to get full codepoint
|
|
80
|
+
const low = str.charCodeAt(i + 1);
|
|
81
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
82
|
+
const cp = (code - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000;
|
|
83
|
+
bytes[offset++] = 0xf0 | (cp >> 18);
|
|
84
|
+
bytes[offset++] = 0x80 | ((cp >> 12) & 0x3f);
|
|
85
|
+
bytes[offset++] = 0x80 | ((cp >> 6) & 0x3f);
|
|
86
|
+
bytes[offset++] = 0x80 | (cp & 0x3f);
|
|
87
|
+
i++; // Skip the low surrogate
|
|
88
|
+
} else {
|
|
89
|
+
// Lone high surrogate: encode as replacement character U+FFFD
|
|
90
|
+
bytes[offset++] = 0xef;
|
|
91
|
+
bytes[offset++] = 0xbf;
|
|
92
|
+
bytes[offset++] = 0xbd;
|
|
93
|
+
}
|
|
94
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
95
|
+
// Lone low surrogate: encode as replacement character U+FFFD
|
|
96
|
+
bytes[offset++] = 0xef;
|
|
97
|
+
bytes[offset++] = 0xbf;
|
|
98
|
+
bytes[offset++] = 0xbd;
|
|
99
|
+
} else {
|
|
100
|
+
bytes[offset++] = 0xe0 | (code >> 12);
|
|
101
|
+
bytes[offset++] = 0x80 | ((code >> 6) & 0x3f);
|
|
102
|
+
bytes[offset++] = 0x80 | (code & 0x3f);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return bytes.slice(0, offset);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return _textEncoder;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getTextDecoder(): { decode(b: BufferSource): string } {
|
|
113
|
+
if (_textDecoder) return _textDecoder;
|
|
114
|
+
|
|
115
|
+
if (typeof TextDecoder !== 'undefined') {
|
|
116
|
+
_textDecoder = new TextDecoder();
|
|
117
|
+
} else {
|
|
118
|
+
// Simple UTF-8 fallback for Hermes
|
|
119
|
+
_textDecoder = {
|
|
120
|
+
decode(buffer: BufferSource): string {
|
|
121
|
+
const bytes = buffer instanceof ArrayBuffer
|
|
122
|
+
? new Uint8Array(buffer)
|
|
123
|
+
: arrayBufferViewToUint8Array(buffer);
|
|
124
|
+
|
|
125
|
+
let result = '';
|
|
126
|
+
let i = 0;
|
|
127
|
+
while (i < bytes.length) {
|
|
128
|
+
const byte = bytes[i];
|
|
129
|
+
if (byte < 0x80) {
|
|
130
|
+
result += String.fromCharCode(byte);
|
|
131
|
+
i++;
|
|
132
|
+
} else if ((byte & 0xe0) === 0xc0) {
|
|
133
|
+
result += String.fromCharCode(((byte & 0x1f) << 6) | (bytes[i + 1] & 0x3f));
|
|
134
|
+
i += 2;
|
|
135
|
+
} else if ((byte & 0xf0) === 0xe0) {
|
|
136
|
+
result += String.fromCharCode(
|
|
137
|
+
((byte & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)
|
|
138
|
+
);
|
|
139
|
+
i += 3;
|
|
140
|
+
} else if ((byte & 0xf8) === 0xf0) {
|
|
141
|
+
// 4-byte sequence: decode full codepoint and convert to surrogate pair
|
|
142
|
+
const cp =
|
|
143
|
+
((byte & 0x07) << 18) |
|
|
144
|
+
((bytes[i + 1] & 0x3f) << 12) |
|
|
145
|
+
((bytes[i + 2] & 0x3f) << 6) |
|
|
146
|
+
(bytes[i + 3] & 0x3f);
|
|
147
|
+
result += String.fromCodePoint(cp);
|
|
148
|
+
i += 4;
|
|
149
|
+
} else {
|
|
150
|
+
// Invalid leading byte: skip it
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return _textDecoder;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getTextDecoderNoBOM(): { decode(b: BufferSource): string } {
|
|
162
|
+
if (_textDecoderNoBOM) return _textDecoderNoBOM;
|
|
163
|
+
|
|
164
|
+
if (typeof TextDecoder !== 'undefined') {
|
|
165
|
+
_textDecoderNoBOM = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
|
|
166
|
+
} else {
|
|
167
|
+
_textDecoderNoBOM = getTextDecoder();
|
|
168
|
+
}
|
|
169
|
+
return _textDecoderNoBOM;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function decodeTextForFormData(buffer: ArrayBuffer): string {
|
|
173
|
+
const text = getTextDecoderNoBOM().decode(new Uint8Array(buffer));
|
|
174
|
+
return text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if a value is a Blob-like object.
|
|
179
|
+
*/
|
|
180
|
+
export function isBlob(value: unknown): value is Blob {
|
|
181
|
+
return (
|
|
182
|
+
typeof value === 'object' &&
|
|
183
|
+
value !== null &&
|
|
184
|
+
typeof (value as Blob).arrayBuffer === 'function' &&
|
|
185
|
+
typeof (value as Blob).slice === 'function' &&
|
|
186
|
+
typeof (value as Blob).size === 'number' &&
|
|
187
|
+
typeof (value as Blob).type === 'string'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* FormData with entries method (for iteration).
|
|
193
|
+
*/
|
|
194
|
+
interface FormDataWithEntries extends FormData {
|
|
195
|
+
entries(): IterableIterator<[string, FormDataEntryValue]>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if a value is FormData-like.
|
|
200
|
+
*/
|
|
201
|
+
export function isFormData(value: unknown): value is FormDataWithEntries {
|
|
202
|
+
return (
|
|
203
|
+
typeof value === 'object' &&
|
|
204
|
+
value !== null &&
|
|
205
|
+
typeof (value as FormData).append === 'function' &&
|
|
206
|
+
typeof (value as FormData & { entries?: unknown }).entries === 'function'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if a value is an ArrayBuffer.
|
|
212
|
+
*/
|
|
213
|
+
export function isArrayBuffer(value: unknown): value is ArrayBuffer {
|
|
214
|
+
return value instanceof ArrayBuffer;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if a value is an ArrayBufferView (TypedArray or DataView).
|
|
219
|
+
*/
|
|
220
|
+
export function isArrayBufferView(value: unknown): value is ArrayBufferView {
|
|
221
|
+
return ArrayBuffer.isView(value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function getArrayBufferViewByteLength(view: ArrayBufferView): number {
|
|
225
|
+
const reportedByteLength = view.byteLength;
|
|
226
|
+
const typedArrayLength = (view as ArrayBufferView & { length?: unknown }).length;
|
|
227
|
+
const bytesPerElement =
|
|
228
|
+
view &&
|
|
229
|
+
view.constructor &&
|
|
230
|
+
typeof (view.constructor as { BYTES_PER_ELEMENT?: unknown }).BYTES_PER_ELEMENT === 'number'
|
|
231
|
+
? (view.constructor as { BYTES_PER_ELEMENT: number }).BYTES_PER_ELEMENT
|
|
232
|
+
: null;
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
typeof typedArrayLength === 'number' &&
|
|
236
|
+
typeof bytesPerElement === 'number'
|
|
237
|
+
) {
|
|
238
|
+
const computedByteLength = typedArrayLength * bytesPerElement;
|
|
239
|
+
if (computedByteLength >= 0 && computedByteLength <= reportedByteLength) {
|
|
240
|
+
return computedByteLength;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return reportedByteLength;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function arrayBufferViewToUint8Array(view: ArrayBufferView): Uint8Array {
|
|
248
|
+
return new Uint8Array(view.buffer, view.byteOffset, getArrayBufferViewByteLength(view));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function normalizeBodyChunk(
|
|
252
|
+
chunk: unknown,
|
|
253
|
+
source = 'ReadableStream body'
|
|
254
|
+
): Uint8Array {
|
|
255
|
+
if (chunk instanceof Uint8Array) {
|
|
256
|
+
return chunk;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (typeof chunk === 'string') {
|
|
260
|
+
return getTextEncoder().encode(chunk);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isArrayBuffer(chunk)) {
|
|
264
|
+
return new Uint8Array(chunk.slice(0));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isArrayBufferView(chunk)) {
|
|
268
|
+
return arrayBufferViewToUint8Array(chunk);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
throw new TypeError(`${source} chunk must be a string, Buffer, or ArrayBufferView.`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function normalizeReadableStreamBody(
|
|
275
|
+
stream: ReadableStream<unknown>,
|
|
276
|
+
source = 'ReadableStream body'
|
|
277
|
+
): ReadableStream<Uint8Array> {
|
|
278
|
+
const reader = stream.getReader();
|
|
279
|
+
reader.closed.catch(function () {});
|
|
280
|
+
let released = false;
|
|
281
|
+
|
|
282
|
+
function releaseReader(): void {
|
|
283
|
+
if (released) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
released = true;
|
|
287
|
+
try {
|
|
288
|
+
reader.releaseLock();
|
|
289
|
+
reader.closed.catch(function () {});
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return new RuntimeReadableStream<Uint8Array>({
|
|
294
|
+
pull(controller) {
|
|
295
|
+
const readPromise = reader.read();
|
|
296
|
+
readPromise.catch(function () {});
|
|
297
|
+
return readPromise.then(
|
|
298
|
+
function (result) {
|
|
299
|
+
if (result.done) {
|
|
300
|
+
releaseReader();
|
|
301
|
+
controller.close();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
controller.enqueue(normalizeBodyChunk(result.value, source));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
try { reader.cancel(error).catch(function () {}); } catch {}
|
|
308
|
+
releaseReader();
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
function (error) {
|
|
313
|
+
releaseReader();
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
},
|
|
318
|
+
cancel(reason) {
|
|
319
|
+
const cancelResult = reader.cancel(reason);
|
|
320
|
+
if (cancelResult && typeof (cancelResult as Promise<void>).then === 'function') {
|
|
321
|
+
return (cancelResult as Promise<void>).then(
|
|
322
|
+
function () {
|
|
323
|
+
releaseReader();
|
|
324
|
+
},
|
|
325
|
+
function () {
|
|
326
|
+
releaseReader();
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
releaseReader();
|
|
331
|
+
return undefined;
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function isAsyncIterableBody(value: unknown): value is AsyncIterable<unknown> {
|
|
337
|
+
return (
|
|
338
|
+
value !== null &&
|
|
339
|
+
typeof value === 'object' &&
|
|
340
|
+
typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function createReadableStreamFromAsyncIterableBody(
|
|
345
|
+
body: AsyncIterable<unknown>,
|
|
346
|
+
source = 'Async iterable body'
|
|
347
|
+
): ReadableStream<Uint8Array> {
|
|
348
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
349
|
+
return new RuntimeReadableStream<Uint8Array>({
|
|
350
|
+
async pull(controller) {
|
|
351
|
+
const result = await iterator.next();
|
|
352
|
+
if (result.done) {
|
|
353
|
+
controller.close();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
controller.enqueue(normalizeBodyChunk(result.value, source));
|
|
357
|
+
},
|
|
358
|
+
async cancel(reason) {
|
|
359
|
+
if (typeof iterator.return === 'function') {
|
|
360
|
+
await iterator.return(reason);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function asyncIterableToUint8Array(
|
|
367
|
+
body: AsyncIterable<unknown>,
|
|
368
|
+
source = 'Async iterable body'
|
|
369
|
+
): Promise<Uint8Array> {
|
|
370
|
+
const chunks: Uint8Array[] = [];
|
|
371
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
372
|
+
let shouldCloseIterator = false;
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
while (true) {
|
|
376
|
+
const result = await iterator.next();
|
|
377
|
+
if (result.done) {
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Track whether we have accepted a chunk but not yet finished handling
|
|
382
|
+
// it so abrupt failures can still signal iterator cleanup.
|
|
383
|
+
shouldCloseIterator = true;
|
|
384
|
+
chunks.push(normalizeBodyChunk(result.value, source));
|
|
385
|
+
shouldCloseIterator = false;
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (shouldCloseIterator && typeof iterator.return === 'function') {
|
|
389
|
+
try {
|
|
390
|
+
await iterator.return();
|
|
391
|
+
} catch {
|
|
392
|
+
// Preserve the original body conversion failure.
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return concatUint8Arrays(chunks);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Convert various body types to Uint8Array.
|
|
403
|
+
*/
|
|
404
|
+
export async function bodyToUint8Array(
|
|
405
|
+
body: BodyInit | null | undefined
|
|
406
|
+
): Promise<Uint8Array | null> {
|
|
407
|
+
if (body === null || body === undefined) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (typeof body === 'string') {
|
|
412
|
+
return getTextEncoder().encode(body);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (isArrayBuffer(body)) {
|
|
416
|
+
return new Uint8Array(body);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (isArrayBufferView(body)) {
|
|
420
|
+
return arrayBufferViewToUint8Array(body);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (isBlob(body)) {
|
|
424
|
+
const buffer = await body.arrayBuffer();
|
|
425
|
+
return new Uint8Array(buffer);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
429
|
+
return getTextEncoder().encode(body.toString());
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (isFormData(body)) {
|
|
433
|
+
// Check cache first (populated by getContentTypeForBody)
|
|
434
|
+
let cached = formDataEncodeCache.get(body);
|
|
435
|
+
if (cached) {
|
|
436
|
+
return cached.body;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Use FormData's built-in multipart encoding if available
|
|
440
|
+
if (typeof (body as any)._encode === 'function') {
|
|
441
|
+
const result = (body as any)._encode();
|
|
442
|
+
formDataEncodeCache.set(body, result);
|
|
443
|
+
return result.body;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Fallback: Convert FormData to multipart/form-data format
|
|
447
|
+
const encoded = await encodeFormDataInternalAsync(body);
|
|
448
|
+
formDataEncodeCache.set(body, encoded);
|
|
449
|
+
return encoded.body;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (isReadableStream(body)) {
|
|
453
|
+
return await readableStreamToUint8Array(normalizeReadableStreamBody(body));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isAsyncIterableBody(body)) {
|
|
457
|
+
return await asyncIterableToUint8Array(body);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
throw new TypeError('Unsupported body type');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Resolve a promise with a value, preventing Object.prototype.then from
|
|
465
|
+
* making the value appear as a thenable (per Fetch spec, internal algorithms
|
|
466
|
+
* should not be affected by prototype pollution).
|
|
467
|
+
*/
|
|
468
|
+
export function resolveWithoutThenable<T>(resolve: (value: T) => void, value: T): void {
|
|
469
|
+
if (value != null && typeof value === 'object' && typeof (value as any).then === 'function') {
|
|
470
|
+
Object.defineProperty(value, 'then', { value: undefined, configurable: true, writable: true });
|
|
471
|
+
resolve(value);
|
|
472
|
+
delete (value as any).then;
|
|
473
|
+
} else {
|
|
474
|
+
resolve(value);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Read a ReadableStream to Uint8Array.
|
|
480
|
+
* Uses explicit promise chains (not async/await) to prevent compiled async
|
|
481
|
+
* wrappers from creating intermediate promises that trigger Object.prototype.then.
|
|
482
|
+
*/
|
|
483
|
+
export function readableStreamToUint8Array(
|
|
484
|
+
stream: ReadableStream<Uint8Array>,
|
|
485
|
+
signal?: AbortSignal | null
|
|
486
|
+
): Promise<Uint8Array> {
|
|
487
|
+
return new Promise<Uint8Array>(function (outerResolve, outerReject) {
|
|
488
|
+
const reader = stream.getReader();
|
|
489
|
+
reader.closed.catch(function () {});
|
|
490
|
+
const chunks: Uint8Array[] = [];
|
|
491
|
+
let settled = false;
|
|
492
|
+
|
|
493
|
+
function cleanupAbortListener(): void {
|
|
494
|
+
if (signal) {
|
|
495
|
+
signal.removeEventListener('abort', abortReader);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function rejectWith(error: unknown): void {
|
|
500
|
+
if (settled) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
settled = true;
|
|
504
|
+
cleanupAbortListener();
|
|
505
|
+
try { reader.cancel(error).catch(function () {}); } catch {}
|
|
506
|
+
try {
|
|
507
|
+
reader.releaseLock();
|
|
508
|
+
reader.closed.catch(function () {});
|
|
509
|
+
} catch {}
|
|
510
|
+
outerReject(error);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function abortReader(): void {
|
|
514
|
+
rejectWith(
|
|
515
|
+
signal?.reason ??
|
|
516
|
+
new DOMException('The operation was aborted.', 'AbortError')
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (signal) {
|
|
521
|
+
if (signal.aborted) {
|
|
522
|
+
abortReader();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
signal.addEventListener('abort', abortReader);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function pump(): void {
|
|
529
|
+
if (settled) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (signal?.aborted) {
|
|
533
|
+
abortReader();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const readPromise = reader.read();
|
|
538
|
+
readPromise.catch(function () {});
|
|
539
|
+
readPromise.then(
|
|
540
|
+
function (result) {
|
|
541
|
+
if (settled) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (result.done) {
|
|
545
|
+
settled = true;
|
|
546
|
+
cleanupAbortListener();
|
|
547
|
+
reader.releaseLock();
|
|
548
|
+
const concatenated = concatUint8Arrays(chunks);
|
|
549
|
+
resolveWithoutThenable(outerResolve, concatenated);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
chunks.push(normalizeBodyChunk(result.value));
|
|
554
|
+
} catch (error) {
|
|
555
|
+
rejectWith(error);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (signal?.aborted) {
|
|
559
|
+
abortReader();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
pump();
|
|
563
|
+
},
|
|
564
|
+
function (err) {
|
|
565
|
+
rejectWith(err);
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
pump();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Concatenate multiple Uint8Arrays into one.
|
|
576
|
+
*/
|
|
577
|
+
export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
578
|
+
const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
|
|
579
|
+
const result = new Uint8Array(totalLength);
|
|
580
|
+
let offset = 0;
|
|
581
|
+
for (const arr of arrays) {
|
|
582
|
+
result.set(arr, offset);
|
|
583
|
+
offset += arr.length;
|
|
584
|
+
}
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Generate a boundary string for multipart/form-data.
|
|
590
|
+
*/
|
|
591
|
+
export function generateBoundary(): string {
|
|
592
|
+
return '----ExactFormBoundary' + Math.random().toString(36).slice(2);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Cache for FormData encoding results to ensure consistent boundaries.
|
|
597
|
+
* Using WeakMap so FormData objects can be garbage collected.
|
|
598
|
+
*/
|
|
599
|
+
const formDataEncodeCache = new WeakMap<FormData, { body: Uint8Array; contentType: string }>();
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get the Content-Type header for a body type.
|
|
603
|
+
* For FormData, this will cache the encoding result so that bodyToUint8Array
|
|
604
|
+
* returns the same bytes with matching boundary.
|
|
605
|
+
*/
|
|
606
|
+
export function getContentTypeForBody(body: BodyInit | null): string | null {
|
|
607
|
+
if (body === null || body === undefined) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (typeof body === 'string') {
|
|
612
|
+
return 'text/plain;charset=UTF-8';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
616
|
+
return 'application/x-www-form-urlencoded;charset=UTF-8';
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (isBlob(body) && body.type) {
|
|
620
|
+
return body.type;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (isFormData(body)) {
|
|
624
|
+
// Check cache first
|
|
625
|
+
let cached = formDataEncodeCache.get(body);
|
|
626
|
+
if (!cached) {
|
|
627
|
+
// Encode once and cache the result
|
|
628
|
+
cached = encodeFormDataInternal(body);
|
|
629
|
+
formDataEncodeCache.set(body, cached);
|
|
630
|
+
}
|
|
631
|
+
return cached.contentType;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// For ArrayBuffer, TypedArray, etc., no default Content-Type
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Internal sync encoding for FormData (uses _getBytes for Blobs).
|
|
640
|
+
*/
|
|
641
|
+
function encodeFormDataInternal(body: FormData): { body: Uint8Array; contentType: string } {
|
|
642
|
+
// Use FormData's built-in encoding if available
|
|
643
|
+
if (typeof (body as any)._encode === 'function') {
|
|
644
|
+
return (body as any)._encode();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const boundary = generateBoundary();
|
|
648
|
+
const parts: Uint8Array[] = [];
|
|
649
|
+
|
|
650
|
+
for (const [name, value] of (body as any).entries()) {
|
|
651
|
+
if (typeof value === 'string') {
|
|
652
|
+
parts.push(getTextEncoder().encode(
|
|
653
|
+
`--${boundary}\r\n` +
|
|
654
|
+
`Content-Disposition: form-data; name="${name}"\r\n\r\n` +
|
|
655
|
+
`${value}\r\n`
|
|
656
|
+
));
|
|
657
|
+
} else if (isBlob(value)) {
|
|
658
|
+
const fileBytes = (value as any)._getBytes?.() ?? new Uint8Array(0);
|
|
659
|
+
const fileName = (value as File).name || 'blob';
|
|
660
|
+
const contentType = value.type || 'application/octet-stream';
|
|
661
|
+
|
|
662
|
+
parts.push(getTextEncoder().encode(
|
|
663
|
+
`--${boundary}\r\n` +
|
|
664
|
+
`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n` +
|
|
665
|
+
`Content-Type: ${contentType}\r\n\r\n`
|
|
666
|
+
));
|
|
667
|
+
parts.push(fileBytes);
|
|
668
|
+
parts.push(getTextEncoder().encode('\r\n'));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Per WPT: empty FormData produces an empty body
|
|
673
|
+
if (parts.length === 0) {
|
|
674
|
+
return {
|
|
675
|
+
body: new Uint8Array(0),
|
|
676
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
parts.push(getTextEncoder().encode(`--${boundary}--\r\n`));
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
body: concatUint8Arrays(parts),
|
|
684
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Internal async encoding for FormData (uses arrayBuffer for Blobs).
|
|
690
|
+
*/
|
|
691
|
+
async function encodeFormDataInternalAsync(body: FormData): Promise<{ body: Uint8Array; contentType: string }> {
|
|
692
|
+
// Use FormData's built-in encoding if available
|
|
693
|
+
if (typeof (body as any)._encode === 'function') {
|
|
694
|
+
return (body as any)._encode();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const boundary = generateBoundary();
|
|
698
|
+
const parts: Uint8Array[] = [];
|
|
699
|
+
|
|
700
|
+
for (const [name, value] of (body as any).entries()) {
|
|
701
|
+
if (typeof value === 'string') {
|
|
702
|
+
parts.push(getTextEncoder().encode(
|
|
703
|
+
`--${boundary}\r\n` +
|
|
704
|
+
`Content-Disposition: form-data; name="${name}"\r\n\r\n` +
|
|
705
|
+
`${value}\r\n`
|
|
706
|
+
));
|
|
707
|
+
} else if (isBlob(value)) {
|
|
708
|
+
const buffer = await value.arrayBuffer();
|
|
709
|
+
const fileBytes = new Uint8Array(buffer);
|
|
710
|
+
const fileName = (value as File).name || 'blob';
|
|
711
|
+
const contentType = value.type || 'application/octet-stream';
|
|
712
|
+
|
|
713
|
+
parts.push(getTextEncoder().encode(
|
|
714
|
+
`--${boundary}\r\n` +
|
|
715
|
+
`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n` +
|
|
716
|
+
`Content-Type: ${contentType}\r\n\r\n`
|
|
717
|
+
));
|
|
718
|
+
parts.push(fileBytes);
|
|
719
|
+
parts.push(getTextEncoder().encode('\r\n'));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Per WPT: empty FormData produces an empty body
|
|
724
|
+
if (parts.length === 0) {
|
|
725
|
+
return {
|
|
726
|
+
body: new Uint8Array(0),
|
|
727
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
parts.push(getTextEncoder().encode(`--${boundary}--\r\n`));
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
body: concatUint8Arrays(parts),
|
|
735
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Encode FormData body with its content type (for consistent boundary).
|
|
741
|
+
* Returns both the body bytes and content-type header.
|
|
742
|
+
*/
|
|
743
|
+
export function encodeFormData(body: FormData): { body: Uint8Array; contentType: string } {
|
|
744
|
+
// Check cache first — getContentTypeForBody() may have already encoded this FormData
|
|
745
|
+
// with a specific boundary. We must reuse it to keep Content-Type and body consistent.
|
|
746
|
+
let cached = formDataEncodeCache.get(body);
|
|
747
|
+
if (cached) return cached;
|
|
748
|
+
|
|
749
|
+
if (typeof (body as any)._encode === 'function') {
|
|
750
|
+
const result = (body as any)._encode();
|
|
751
|
+
formDataEncodeCache.set(body, result);
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Fallback encoding
|
|
756
|
+
const result = encodeFormDataInternal(body);
|
|
757
|
+
formDataEncodeCache.set(body, result);
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Parse array buffer as JSON.
|
|
763
|
+
*/
|
|
764
|
+
export function parseJson(buffer: ArrayBuffer): unknown {
|
|
765
|
+
// Per Fetch spec: reject UTF-16 encoded content (only UTF-8 is valid for JSON)
|
|
766
|
+
const bytes = new Uint8Array(buffer);
|
|
767
|
+
if (bytes.length >= 2) {
|
|
768
|
+
// UTF-16 BE BOM: FE FF, or UTF-16 LE BOM: FF FE
|
|
769
|
+
if ((bytes[0] === 0xFE && bytes[1] === 0xFF) || (bytes[0] === 0xFF && bytes[1] === 0xFE)) {
|
|
770
|
+
throw new SyntaxError('Invalid JSON: UTF-16 encoded content is not valid');
|
|
771
|
+
}
|
|
772
|
+
// Detect BOM-less UTF-16: valid JSON always starts with an ASCII character
|
|
773
|
+
// (whitespace, '{', '[', '"', digit, 't', 'f', 'n'). In UTF-16 LE the
|
|
774
|
+
// second byte is 0x00, in UTF-16 BE the first byte is 0x00.
|
|
775
|
+
if (bytes[0] === 0x00 || bytes[1] === 0x00) {
|
|
776
|
+
throw new SyntaxError('Invalid JSON: UTF-16 encoded content is not valid');
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const text = getTextDecoder().decode(buffer);
|
|
780
|
+
return JSON.parse(text);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Parse array buffer as text.
|
|
785
|
+
*/
|
|
786
|
+
export function parseText(buffer: ArrayBuffer): string {
|
|
787
|
+
return getTextDecoder().decode(buffer);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Create a ReadableStream from a Uint8Array.
|
|
792
|
+
* Returns null if ReadableStream is not available (falls back to buffer-based reading).
|
|
793
|
+
*/
|
|
794
|
+
export function createReadableStreamFromUint8Array(
|
|
795
|
+
data: Uint8Array | null
|
|
796
|
+
): ReadableStream<Uint8Array> | null {
|
|
797
|
+
if (data === null) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Byte streams may detach the enqueued chunk's backing buffer. Keep the
|
|
802
|
+
// caller-owned buffer stable by enqueueing a cloned view instead.
|
|
803
|
+
const streamData = new Uint8Array(data.byteLength);
|
|
804
|
+
streamData.set(data);
|
|
805
|
+
|
|
806
|
+
const streamCtor =
|
|
807
|
+
(globalThis as any).ReadableStream || RuntimeReadableStream;
|
|
808
|
+
|
|
809
|
+
// Check if a ReadableStream constructor is available - it might not be in
|
|
810
|
+
// all environments.
|
|
811
|
+
if (typeof streamCtor !== 'function') {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Create a byte stream (type: 'bytes') so that BYOB readers work
|
|
816
|
+
return new (streamCtor as typeof ReadableStream)({
|
|
817
|
+
type: 'bytes',
|
|
818
|
+
start(controller: any) {
|
|
819
|
+
if (streamData.byteLength > 0) {
|
|
820
|
+
controller.enqueue(streamData);
|
|
821
|
+
}
|
|
822
|
+
controller.close();
|
|
823
|
+
},
|
|
824
|
+
} as any);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Extract the boundary parameter from a multipart/form-data Content-Type header.
|
|
829
|
+
* @param contentType The Content-Type header value
|
|
830
|
+
* @returns The boundary string, or null if not found
|
|
831
|
+
*/
|
|
832
|
+
export function extractBoundary(contentType: string): string | null {
|
|
833
|
+
// Match boundary parameter: boundary=VALUE or boundary="VALUE"
|
|
834
|
+
const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
|
|
835
|
+
if (!match) return null;
|
|
836
|
+
return match[1] ?? match[2] ?? null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Find the index of a byte sequence (needle) within a Uint8Array (haystack),
|
|
841
|
+
* starting from the given offset.
|
|
842
|
+
*/
|
|
843
|
+
function indexOfBytes(haystack: Uint8Array, needle: Uint8Array, offset: number = 0): number {
|
|
844
|
+
const haystackLen = haystack.length;
|
|
845
|
+
const needleLen = needle.length;
|
|
846
|
+
if (needleLen === 0) return offset;
|
|
847
|
+
if (offset + needleLen > haystackLen) return -1;
|
|
848
|
+
|
|
849
|
+
outer:
|
|
850
|
+
for (let i = offset; i <= haystackLen - needleLen; i++) {
|
|
851
|
+
for (let j = 0; j < needleLen; j++) {
|
|
852
|
+
if (haystack[i + j] !== needle[j]) {
|
|
853
|
+
continue outer;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return i;
|
|
857
|
+
}
|
|
858
|
+
return -1;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Parse a Content-Disposition header value to extract name and filename parameters.
|
|
863
|
+
*/
|
|
864
|
+
function parseContentDisposition(header: string): { name: string; filename?: string } {
|
|
865
|
+
let name = '';
|
|
866
|
+
let filename: string | undefined;
|
|
867
|
+
|
|
868
|
+
// Extract name parameter
|
|
869
|
+
const nameMatch = header.match(/\bname=(?:"([^"]*(?:\\.[^"]*)*)"|([^\s;]+))/i);
|
|
870
|
+
if (nameMatch) {
|
|
871
|
+
name = (nameMatch[1] ?? nameMatch[2] ?? '').replace(/\\"/g, '"');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Extract filename parameter
|
|
875
|
+
const filenameMatch = header.match(/\bfilename=(?:"([^"]*(?:\\.[^"]*)*)"|([^\s;]+))/i);
|
|
876
|
+
if (filenameMatch) {
|
|
877
|
+
filename = (filenameMatch[1] ?? filenameMatch[2] ?? '').replace(/\\"/g, '"');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return { name, filename };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Parse the headers section of a multipart part.
|
|
885
|
+
* Returns a map of lowercased header names to their values.
|
|
886
|
+
*/
|
|
887
|
+
function parsePartHeaders(headerBytes: Uint8Array): Map<string, string> {
|
|
888
|
+
const decoder = getTextDecoder();
|
|
889
|
+
const headerText = decoder.decode(headerBytes);
|
|
890
|
+
const headers = new Map<string, string>();
|
|
891
|
+
|
|
892
|
+
// Split by CRLF, handling potential line folding
|
|
893
|
+
const lines = headerText.split('\r\n');
|
|
894
|
+
for (const line of lines) {
|
|
895
|
+
if (!line) continue;
|
|
896
|
+
const colonIndex = line.indexOf(':');
|
|
897
|
+
if (colonIndex === -1) continue;
|
|
898
|
+
const name = line.slice(0, colonIndex).trim().toLowerCase();
|
|
899
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
900
|
+
headers.set(name, value);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return headers;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Parse a multipart/form-data body into a FormData object.
|
|
908
|
+
*
|
|
909
|
+
* @param body The raw body bytes
|
|
910
|
+
* @param boundary The boundary string from the Content-Type header
|
|
911
|
+
* @returns A populated FormData object
|
|
912
|
+
*/
|
|
913
|
+
export function parseMultipartFormData(body: Uint8Array, boundary: string): FormData {
|
|
914
|
+
const encoder = getTextEncoder();
|
|
915
|
+
const formData = new FormData();
|
|
916
|
+
|
|
917
|
+
// Boundary markers
|
|
918
|
+
const dashBoundary = encoder.encode('--' + boundary);
|
|
919
|
+
const crlf = encoder.encode('\r\n');
|
|
920
|
+
const doubleCrlf = encoder.encode('\r\n\r\n');
|
|
921
|
+
|
|
922
|
+
// Find the first boundary
|
|
923
|
+
let pos = indexOfBytes(body, dashBoundary, 0);
|
|
924
|
+
if (pos === -1) {
|
|
925
|
+
throw new TypeError('Failed to parse multipart/form-data body: boundary not found');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Move past the first boundary line
|
|
929
|
+
pos += dashBoundary.length;
|
|
930
|
+
|
|
931
|
+
// Check if immediately followed by -- (closing boundary with no parts)
|
|
932
|
+
if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {
|
|
933
|
+
return formData;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Skip CRLF after boundary
|
|
937
|
+
if (pos + 2 <= body.length && body[pos] === 0x0d && body[pos + 1] === 0x0a) {
|
|
938
|
+
pos += 2;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let foundClosingBoundary = false;
|
|
942
|
+
|
|
943
|
+
while (pos < body.length) {
|
|
944
|
+
// Find the end of headers (double CRLF)
|
|
945
|
+
const headersEnd = indexOfBytes(body, doubleCrlf, pos);
|
|
946
|
+
if (headersEnd === -1) break;
|
|
947
|
+
|
|
948
|
+
// Parse headers for this part
|
|
949
|
+
const headerBytes = body.slice(pos, headersEnd);
|
|
950
|
+
const headers = parsePartHeaders(headerBytes);
|
|
951
|
+
|
|
952
|
+
// Move past the double CRLF to the start of the body
|
|
953
|
+
const bodyStart = headersEnd + doubleCrlf.length;
|
|
954
|
+
|
|
955
|
+
// Find the next boundary to determine where this part's body ends
|
|
956
|
+
const nextBoundary = indexOfBytes(body, dashBoundary, bodyStart);
|
|
957
|
+
if (nextBoundary === -1) break;
|
|
958
|
+
|
|
959
|
+
// The body ends before the CRLF preceding the boundary
|
|
960
|
+
// The format is: body\r\n--boundary
|
|
961
|
+
let bodyEnd = nextBoundary;
|
|
962
|
+
if (bodyEnd >= 2 && body[bodyEnd - 2] === 0x0d && body[bodyEnd - 1] === 0x0a) {
|
|
963
|
+
bodyEnd -= 2;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const partBody = body.slice(bodyStart, bodyEnd);
|
|
967
|
+
|
|
968
|
+
// Parse Content-Disposition to get name and filename
|
|
969
|
+
const disposition = headers.get('content-disposition') ?? '';
|
|
970
|
+
const { name, filename } = parseContentDisposition(disposition);
|
|
971
|
+
|
|
972
|
+
if (!name) {
|
|
973
|
+
// Skip parts without a name (per spec)
|
|
974
|
+
pos = nextBoundary + dashBoundary.length;
|
|
975
|
+
// Check for closing boundary
|
|
976
|
+
if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {
|
|
977
|
+
foundClosingBoundary = true;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
// Skip CRLF
|
|
981
|
+
if (pos + 2 <= body.length && body[pos] === 0x0d && body[pos + 1] === 0x0a) pos += 2;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (filename !== undefined) {
|
|
986
|
+
// File field: create a File object
|
|
987
|
+
const contentType = headers.get('content-type') ?? 'application/octet-stream';
|
|
988
|
+
const file = new File([partBody], filename, { type: contentType });
|
|
989
|
+
formData.append(name, file);
|
|
990
|
+
} else {
|
|
991
|
+
// Text field: decode as UTF-8
|
|
992
|
+
const decoder = getTextDecoder();
|
|
993
|
+
const text = decoder.decode(partBody);
|
|
994
|
+
formData.append(name, text);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Move past the boundary
|
|
998
|
+
pos = nextBoundary + dashBoundary.length;
|
|
999
|
+
|
|
1000
|
+
// Check for closing boundary (--)
|
|
1001
|
+
if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {
|
|
1002
|
+
foundClosingBoundary = true;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Skip CRLF after boundary
|
|
1007
|
+
if (pos + 2 <= body.length && body[pos] === 0x0d && body[pos + 1] === 0x0a) {
|
|
1008
|
+
pos += 2;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Per spec: if the multipart body doesn't have a valid closing boundary, reject
|
|
1013
|
+
if (!foundClosingBoundary) {
|
|
1014
|
+
throw new TypeError('Failed to parse multipart/form-data body: missing closing boundary');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return formData;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Body mixin base class.
|
|
1022
|
+
* Provides common body handling functionality for Request and Response.
|
|
1023
|
+
*/
|
|
1024
|
+
export abstract class BodyMixin {
|
|
1025
|
+
protected _body: ReadableStream<Uint8Array> | null = null;
|
|
1026
|
+
protected _bodyUsed = false;
|
|
1027
|
+
protected _bodyBuffer: ArrayBuffer | null = null;
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Per Fetch spec: the abort signal associated with the fetch that created
|
|
1031
|
+
* this response. Body consumption methods check this and reject with the
|
|
1032
|
+
* abort reason if the signal is aborted.
|
|
1033
|
+
*/
|
|
1034
|
+
public _signal: AbortSignal | null = null;
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* A ReadableStream of the body contents.
|
|
1038
|
+
*/
|
|
1039
|
+
get body(): ReadableStream<Uint8Array> | null {
|
|
1040
|
+
return this._body;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* A Boolean that indicates whether the body has been read.
|
|
1045
|
+
* Per spec: also true when the body stream has been disturbed (read from)
|
|
1046
|
+
* or locked (e.g., via getReader() or pipeTo()).
|
|
1047
|
+
*/
|
|
1048
|
+
get bodyUsed(): boolean {
|
|
1049
|
+
if (this._bodyUsed) return true;
|
|
1050
|
+
if (this._body !== null) {
|
|
1051
|
+
if ((this._body as any)._disturbed) return true;
|
|
1052
|
+
}
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Whether this object has a body. Subclasses can override to check
|
|
1058
|
+
* additional state (e.g., Request._bodyInit).
|
|
1059
|
+
*/
|
|
1060
|
+
protected _hasBody(): boolean {
|
|
1061
|
+
return this._body !== null || this._bodyBuffer !== null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Determine whether a body read should fail before attempting consumption.
|
|
1066
|
+
* Bun compat expects a reused body to report "Body already used" even if the
|
|
1067
|
+
* underlying stream is already locked from the prior read.
|
|
1068
|
+
*/
|
|
1069
|
+
protected _getBodyReadPreconditionError(): TypeError | null {
|
|
1070
|
+
if (this.bodyUsed) {
|
|
1071
|
+
return new BodyConsumedError();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (this._body !== null && typeof (this._body as any).locked !== 'undefined' && (this._body as any).locked) {
|
|
1075
|
+
return new TypeError(
|
|
1076
|
+
isBunCompatFetchTest() ? 'ReadableStream is locked' : 'Failed to execute: body stream is locked'
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Mark body as used and throw if already used.
|
|
1085
|
+
* Per spec: consuming a null body returns empty without setting bodyUsed.
|
|
1086
|
+
*/
|
|
1087
|
+
protected _consumeBody(): void {
|
|
1088
|
+
if (this.bodyUsed) {
|
|
1089
|
+
throw new BodyConsumedError();
|
|
1090
|
+
}
|
|
1091
|
+
// Per spec: only set bodyUsed if there is actually a body
|
|
1092
|
+
if (this._hasBody()) {
|
|
1093
|
+
this._bodyUsed = true;
|
|
1094
|
+
}
|
|
1095
|
+
// Per Fetch spec: consuming a body locks its stream.
|
|
1096
|
+
// When _bodyBuffer is cached, _getBodyBuffer() won't read the stream,
|
|
1097
|
+
// so we must lock it here. For stream-only bodies (_bodyBuffer === null),
|
|
1098
|
+
// readableStreamToUint8Array() will lock the stream when it runs.
|
|
1099
|
+
if (this._body !== null && this._bodyBuffer !== null && !this._body.locked) {
|
|
1100
|
+
(this._body as any)._disturbed = true;
|
|
1101
|
+
try { this._body.getReader(); } catch {}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Get the body as ArrayBuffer.
|
|
1107
|
+
* Must be implemented by subclasses.
|
|
1108
|
+
*/
|
|
1109
|
+
protected abstract _getBodyBuffer(): Promise<ArrayBuffer>;
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Get the Content-Type header value.
|
|
1113
|
+
* Must be implemented by subclasses to support multipart formData() parsing.
|
|
1114
|
+
*/
|
|
1115
|
+
protected abstract _getContentType(): string | null;
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Per Fetch spec: check if the associated abort signal is aborted, and if so
|
|
1119
|
+
* reject with the signal's reason.
|
|
1120
|
+
*/
|
|
1121
|
+
protected _checkAbortSignal(): Error | null {
|
|
1122
|
+
if (this._signal && this._signal.aborted) {
|
|
1123
|
+
return this._signal.reason ??
|
|
1124
|
+
new DOMException('The operation was aborted.', 'AbortError');
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Returns a promise that resolves with an ArrayBuffer.
|
|
1131
|
+
* Note: uses explicit promise chain (not async) to avoid compiled async
|
|
1132
|
+
* wrapper creating intermediate promises that trigger unhandled rejection tracking.
|
|
1133
|
+
*/
|
|
1134
|
+
arrayBuffer(): Promise<ArrayBuffer> {
|
|
1135
|
+
const preconditionError = this._getBodyReadPreconditionError();
|
|
1136
|
+
if (preconditionError) {
|
|
1137
|
+
return Promise.reject(preconditionError);
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
this._consumeBody();
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
return Promise.reject(e);
|
|
1143
|
+
}
|
|
1144
|
+
return this._getBodyBuffer();
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Returns a promise that resolves with a Blob.
|
|
1149
|
+
*/
|
|
1150
|
+
blob(): Promise<Blob> {
|
|
1151
|
+
const preconditionError = this._getBodyReadPreconditionError();
|
|
1152
|
+
if (preconditionError) {
|
|
1153
|
+
return Promise.reject(preconditionError);
|
|
1154
|
+
}
|
|
1155
|
+
return this.arrayBuffer().then((buffer) => {
|
|
1156
|
+
const type = normalizeBlobContentType(this._getContentType());
|
|
1157
|
+
return type ? new Blob([buffer], { type }) : new Blob([buffer]);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Returns a promise that resolves with a Uint8Array.
|
|
1163
|
+
*/
|
|
1164
|
+
bytes(): Promise<Uint8Array> {
|
|
1165
|
+
const preconditionError = this._getBodyReadPreconditionError();
|
|
1166
|
+
if (preconditionError) {
|
|
1167
|
+
return Promise.reject(preconditionError);
|
|
1168
|
+
}
|
|
1169
|
+
return this.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Returns a promise that resolves with a FormData.
|
|
1174
|
+
* Supports both multipart/form-data and application/x-www-form-urlencoded.
|
|
1175
|
+
*/
|
|
1176
|
+
formData(): Promise<FormData> {
|
|
1177
|
+
const preconditionError = this._getBodyReadPreconditionError();
|
|
1178
|
+
if (preconditionError) {
|
|
1179
|
+
return Promise.reject(preconditionError);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
this._consumeBody();
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
return Promise.reject(e);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const contentType = this._getContentType();
|
|
1189
|
+
const ct = contentType?.toLowerCase() ?? '';
|
|
1190
|
+
const isMultipart = ct.startsWith('multipart/form-data');
|
|
1191
|
+
const isUrlEncoded = ct.startsWith('application/x-www-form-urlencoded');
|
|
1192
|
+
|
|
1193
|
+
const self = this;
|
|
1194
|
+
// Read the body buffer first so that stream errors propagate before
|
|
1195
|
+
// content-type checks (per spec and response-error-from-stream WPT tests).
|
|
1196
|
+
return this._getBodyBuffer().then(function (buffer) {
|
|
1197
|
+
if (!isMultipart && !isUrlEncoded) {
|
|
1198
|
+
throw new TypeError(
|
|
1199
|
+
"Failed to execute 'formData': Content-Type is not 'multipart/form-data' or 'application/x-www-form-urlencoded'"
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
// multipart/form-data
|
|
1203
|
+
if (isMultipart) {
|
|
1204
|
+
const boundary = extractBoundary(contentType!);
|
|
1205
|
+
if (!boundary) {
|
|
1206
|
+
throw new TypeError('multipart/form-data Content-Type missing boundary');
|
|
1207
|
+
}
|
|
1208
|
+
const bytes = new Uint8Array(buffer);
|
|
1209
|
+
// Empty body handling: only return empty FormData if the body was
|
|
1210
|
+
// explicitly set (e.g. new Response(new FormData())). A null body
|
|
1211
|
+
// with a multipart Content-Type header should reject per spec.
|
|
1212
|
+
if (bytes.length === 0) {
|
|
1213
|
+
if (self._hasBody()) {
|
|
1214
|
+
return new FormData();
|
|
1215
|
+
}
|
|
1216
|
+
throw new TypeError('Could not parse content as FormData.');
|
|
1217
|
+
}
|
|
1218
|
+
return parseMultipartFormData(bytes, boundary);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// application/x-www-form-urlencoded
|
|
1222
|
+
const text = decodeTextForFormData(buffer);
|
|
1223
|
+
const formData = new FormData();
|
|
1224
|
+
const params = new URLSearchParams(text);
|
|
1225
|
+
// Use indexed iteration to avoid the Rolldown for-of → forEach transform
|
|
1226
|
+
// which rewrites the loop body into a regular function, breaking returns.
|
|
1227
|
+
const paramEntries = Array.from(params);
|
|
1228
|
+
for (let i = 0; i < paramEntries.length; i++) {
|
|
1229
|
+
formData.append(paramEntries[i][0], paramEntries[i][1]);
|
|
1230
|
+
}
|
|
1231
|
+
return formData;
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Returns a promise that resolves with the result of parsing the body as JSON.
|
|
1237
|
+
*/
|
|
1238
|
+
json(): Promise<unknown> {
|
|
1239
|
+
const preconditionError = this._getBodyReadPreconditionError();
|
|
1240
|
+
if (preconditionError) {
|
|
1241
|
+
return Promise.reject(preconditionError);
|
|
1242
|
+
}
|
|
1243
|
+
return this.arrayBuffer().then(parseJson);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Returns a promise that resolves with a string (decoded as UTF-8).
|
|
1248
|
+
*/
|
|
1249
|
+
text(): Promise<string> {
|
|
1250
|
+
const preconditionError = this._getBodyReadPreconditionError();
|
|
1251
|
+
if (preconditionError) {
|
|
1252
|
+
return Promise.reject(preconditionError);
|
|
1253
|
+
}
|
|
1254
|
+
return this.arrayBuffer().then(parseText);
|
|
1255
|
+
}
|
|
1256
|
+
}
|