@gjsify/fetch 0.0.4 → 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.
Files changed (88) hide show
  1. package/README.md +27 -2
  2. package/globals.mjs +12 -0
  3. package/lib/body.d.ts +69 -0
  4. package/lib/body.js +375 -0
  5. package/lib/errors/abort-error.d.ts +7 -0
  6. package/lib/errors/abort-error.js +9 -0
  7. package/lib/errors/base.d.ts +6 -0
  8. package/lib/errors/base.js +17 -0
  9. package/lib/errors/fetch-error.d.ts +16 -0
  10. package/lib/errors/fetch-error.js +23 -0
  11. package/lib/esm/body.js +104 -56
  12. package/lib/esm/errors/base.js +3 -1
  13. package/lib/esm/headers.js +116 -131
  14. package/lib/esm/index.js +145 -190
  15. package/lib/esm/request.js +42 -41
  16. package/lib/esm/response.js +19 -4
  17. package/lib/esm/utils/blob-from.js +2 -98
  18. package/lib/esm/utils/data-uri.js +23 -0
  19. package/lib/esm/utils/is.js +7 -3
  20. package/lib/esm/utils/multipart-parser.js +5 -2
  21. package/lib/esm/utils/referrer.js +10 -10
  22. package/lib/esm/utils/soup-helpers.js +22 -0
  23. package/lib/headers.d.ts +33 -0
  24. package/lib/headers.js +195 -0
  25. package/lib/index.d.ts +18 -0
  26. package/lib/index.js +205 -0
  27. package/lib/request.d.ts +101 -0
  28. package/lib/request.js +308 -0
  29. package/lib/response.d.ts +73 -0
  30. package/lib/response.js +158 -0
  31. package/lib/types/index.d.ts +1 -0
  32. package/lib/types/index.js +1 -0
  33. package/lib/types/system-error.d.ts +11 -0
  34. package/lib/types/system-error.js +2 -0
  35. package/lib/utils/blob-from.d.ts +2 -0
  36. package/lib/utils/blob-from.js +4 -0
  37. package/lib/utils/data-uri.d.ts +10 -0
  38. package/lib/utils/data-uri.js +27 -0
  39. package/lib/utils/get-search.d.ts +1 -0
  40. package/lib/utils/get-search.js +8 -0
  41. package/lib/utils/is-redirect.d.ts +7 -0
  42. package/lib/utils/is-redirect.js +10 -0
  43. package/lib/utils/is.d.ts +35 -0
  44. package/lib/utils/is.js +74 -0
  45. package/lib/utils/multipart-parser.d.ts +2 -0
  46. package/lib/utils/multipart-parser.js +396 -0
  47. package/lib/utils/referrer.d.ts +76 -0
  48. package/lib/utils/referrer.js +283 -0
  49. package/lib/utils/soup-helpers.d.ts +12 -0
  50. package/lib/utils/soup-helpers.js +25 -0
  51. package/package.json +23 -27
  52. package/src/body.ts +181 -169
  53. package/src/errors/base.ts +3 -1
  54. package/src/headers.ts +155 -202
  55. package/src/index.spec.ts +268 -3
  56. package/src/index.ts +199 -312
  57. package/src/request.ts +84 -75
  58. package/src/response.ts +48 -18
  59. package/src/test.mts +1 -1
  60. package/src/utils/blob-from.ts +4 -164
  61. package/src/utils/data-uri.ts +29 -0
  62. package/src/utils/is.ts +15 -15
  63. package/src/utils/multipart-parser.ts +3 -3
  64. package/src/utils/referrer.ts +11 -11
  65. package/src/utils/soup-helpers.ts +37 -0
  66. package/tsconfig.json +4 -4
  67. package/tsconfig.tsbuildinfo +1 -0
  68. package/lib/cjs/body.js +0 -255
  69. package/lib/cjs/errors/abort-error.js +0 -9
  70. package/lib/cjs/errors/base.js +0 -17
  71. package/lib/cjs/errors/fetch-error.js +0 -21
  72. package/lib/cjs/headers.js +0 -202
  73. package/lib/cjs/index.js +0 -224
  74. package/lib/cjs/request.js +0 -281
  75. package/lib/cjs/response.js +0 -133
  76. package/lib/cjs/types/index.js +0 -1
  77. package/lib/cjs/types/system-error.js +0 -1
  78. package/lib/cjs/utils/blob-from.js +0 -101
  79. package/lib/cjs/utils/get-search.js +0 -11
  80. package/lib/cjs/utils/is-redirect.js +0 -7
  81. package/lib/cjs/utils/is.js +0 -28
  82. package/lib/cjs/utils/multipart-parser.js +0 -353
  83. package/lib/cjs/utils/referrer.js +0 -153
  84. package/test.gjs.js +0 -34758
  85. package/test.gjs.mjs +0 -53172
  86. package/test.node.js +0 -1226
  87. package/test.node.mjs +0 -6273
  88. package/tsconfig.types.json +0 -8
package/src/body.ts CHANGED
@@ -1,19 +1,15 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from node-fetch (https://github.com/node-fetch/node-fetch/blob/main/src/body.js)
3
+ // Copyright (c) node-fetch contributors. MIT license.
4
+ // Modifications: Rewritten for GJS using libsoup 3.0 and @gjsify/url
1
5
 
2
- /**
3
- * Body.js
4
- *
5
- * Body interface provides common methods for Request and Response
6
- */
7
-
8
- import { URLSearchParams } from '@gjsify/deno-runtime/ext/url/00_url';
9
- import { Blob } from "@gjsify/deno-runtime/ext/web/09_file";
6
+ import { URLSearchParams } from '@gjsify/url';
7
+ import { Blob } from './utils/blob-from.js';
10
8
 
11
- import { PassThrough, pipeline as pipelineCb, Readable, Stream, Writable } from 'stream';
12
- import { ReadableStream as StreamWebReadableStream } from "stream/web";
13
- import { types, deprecate, promisify } from 'util';
14
- import { Buffer } from 'buffer';
9
+ import { PassThrough, pipeline as pipelineCb, Readable, Stream, Writable } from 'node:stream';
10
+ import { Buffer } from 'node:buffer';
15
11
 
16
- import { FormData, formDataToBlob } from 'formdata-polyfill/esm.min.js';
12
+ import { FormData, formDataToBlob } from '@gjsify/formdata';
17
13
 
18
14
  import { FetchError } from './errors/fetch-error.js';
19
15
  import { FetchBaseError } from './errors/base.js';
@@ -21,10 +17,33 @@ import { isBlob, isURLSearchParameters } from './utils/is.js';
21
17
 
22
18
  import type { Request } from './request.js';
23
19
  import type { Response } from './response.js';
20
+ import type { SystemError } from './types/index.js';
21
+
22
+ const pipeline = (source: Readable, dest: Writable): Promise<void> =>
23
+ new Promise((resolve, reject) => {
24
+ pipelineCb(source, dest, (err) => {
25
+ if (err) reject(err);
26
+ else resolve();
27
+ });
28
+ });
24
29
 
25
- const pipeline = promisify(pipelineCb);
26
30
  const INTERNALS = Symbol('Body internals');
27
-
31
+
32
+ function isAnyArrayBuffer(val: unknown): val is ArrayBuffer {
33
+ return val instanceof ArrayBuffer ||
34
+ (typeof SharedArrayBuffer !== 'undefined' && val instanceof SharedArrayBuffer);
35
+ }
36
+
37
+ function isBoxedPrimitive(val: unknown): boolean {
38
+ return (
39
+ val instanceof String ||
40
+ val instanceof Number ||
41
+ val instanceof Boolean ||
42
+ (typeof Symbol !== 'undefined' && val instanceof Symbol) ||
43
+ (typeof BigInt !== 'undefined' && val instanceof (BigInt as unknown as typeof Number))
44
+ );
45
+ }
46
+
28
47
  /**
29
48
  * Body mixin
30
49
  *
@@ -33,7 +52,7 @@ const INTERNALS = Symbol('Body internals');
33
52
  * @param body Readable stream
34
53
  * @param opts Response options
35
54
  */
36
- export default class Body implements globalThis.Body {
55
+ export default class Body {
37
56
 
38
57
  [INTERNALS]: {
39
58
  body: null | Buffer | Readable | Blob;
@@ -51,9 +70,9 @@ export default class Body implements globalThis.Body {
51
70
 
52
71
  size = 0;
53
72
 
54
- constructor(body: BodyInit | Readable | Blob | Buffer, options: ResponseInit & { size?: number } = { size: 0 }) {
73
+ constructor(body: BodyInit | Readable | Blob | Buffer | null, options: { size?: number; headers?: unknown } = { size: 0 }) {
55
74
  this.size = options.size || 0;
56
- if (body === null) {
75
+ if (body === null || body === undefined) {
57
76
  // Body is undefined or null
58
77
  this[INTERNALS].body = null;
59
78
  } else if (isURLSearchParameters(body)) {
@@ -61,61 +80,82 @@ export default class Body implements globalThis.Body {
61
80
  this[INTERNALS].body = Buffer.from(body.toString())
62
81
  } else if (isBlob(body)) {
63
82
  // Body is blob
83
+ this[INTERNALS].body = body as Blob;
64
84
  } else if (Buffer.isBuffer(body)) {
65
85
  // Body is Buffer
66
- } else if (types.isAnyArrayBuffer(body)) {
86
+ this[INTERNALS].body = body;
87
+ } else if (isAnyArrayBuffer(body)) {
67
88
  // Body is ArrayBuffer
68
89
  this[INTERNALS].body = Buffer.from(body);
69
90
  } else if (ArrayBuffer.isView(body)) {
70
91
  // Body is ArrayBufferView
71
92
  this[INTERNALS].body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
72
93
  } else if (body instanceof Readable) {
73
- // Body is stream
94
+ // Body is Node.js stream
74
95
  this[INTERNALS].body = body;
75
- } else if (body instanceof ReadableStream || body instanceof StreamWebReadableStream) {
76
- // Body is web stream
77
- this[INTERNALS].body = Readable.fromWeb(body as StreamWebReadableStream); // TODO check compatibility between ReadableStream (from lib.dom.d.ts) and StreamWebReadableStream (from stream/web)
96
+ } else if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {
97
+ // Body is Web ReadableStream — convert to Node.js Readable
98
+ this[INTERNALS].body = readableStreamToReadable(body);
78
99
  } else if (body instanceof FormData) {
79
100
  // Body is FormData
80
- this[INTERNALS].body = formDataToBlob(body) as Blob & globalThis.Blob;
81
- this[INTERNALS].boundary = this[INTERNALS].body.type.split('=')[1];
101
+ const blob = formDataToBlob(body) as Blob & globalThis.Blob;
102
+ this[INTERNALS].body = blob;
103
+ this[INTERNALS].boundary = blob.type?.split('boundary=')?.[1] ?? '';
82
104
  } else if (typeof body === 'string'){
83
- // None of the above
84
- // coerce to string then buffer
105
+ // String body
85
106
  this[INTERNALS].body = Buffer.from(body);
86
107
  } else if (body instanceof URLSearchParams){
87
- // None of the above
88
- // coerce to string then buffer
89
108
  this[INTERNALS].body = Buffer.from(body.toString());
90
109
  } else {
91
110
  console.warn(`Unknown body type "${typeof body}", try to parse the body to string!`);
92
- this[INTERNALS].body = Readable.from(typeof (body as any).toString === 'function' ? (body as any).toString() : body as any); // TODO
111
+ this[INTERNALS].body = Buffer.from(String(body));
93
112
  }
94
113
 
95
- // ´this[INTERNALS].stream = body;
96
-
97
- if (Buffer.isBuffer(body)) {
98
- this[INTERNALS].stream = Readable.from(body);
99
- } else if (isBlob(body)) {
100
- // @ts-ignore
101
- this[INTERNALS].stream = Readable.from(body.stream());
102
- } else if (body instanceof Readable) {
103
- this[INTERNALS].stream = body;
114
+ // Set up the internal stream
115
+ const b = this[INTERNALS].body;
116
+ if (Buffer.isBuffer(b)) {
117
+ this[INTERNALS].stream = Readable.from(b);
118
+ } else if (isBlob(b)) {
119
+ this[INTERNALS].stream = Readable.from(blobToAsyncIterable(b as Blob));
120
+ } else if (b instanceof Readable) {
121
+ this[INTERNALS].stream = b;
104
122
  }
105
123
 
106
- if (body instanceof Stream) {
107
- body.on('error', error_ => {
108
- const error = error_ instanceof FetchBaseError ?
109
- error_ :
110
- new FetchError(`Invalid response body while trying to fetch ${(this as any).url}: ${error_.message}`, 'system', error_ as any);
124
+ if (b instanceof Stream) {
125
+ b.on('error', (error_: Error) => {
126
+ const error = error_ instanceof FetchBaseError
127
+ ? error_
128
+ : new FetchError(`Invalid response body while trying to fetch ${(this as unknown as Request).url}: ${error_.message}`, 'system', error_ as unknown as SystemError);
111
129
  this[INTERNALS].error = error;
112
130
  });
113
131
  }
114
132
  }
115
133
 
116
- get body(): ReadableStream<Uint8Array> {
117
- // @ts-ignore
118
- return Readable.toWeb(this[INTERNALS].stream);
134
+ get body(): ReadableStream<Uint8Array> | null {
135
+ const stream = this[INTERNALS].stream;
136
+ if (!stream) return null;
137
+
138
+ // If ReadableStream is available, wrap the Readable into one
139
+ if (typeof ReadableStream !== 'undefined') {
140
+ return new ReadableStream<Uint8Array>({
141
+ start(controller) {
142
+ stream.on('data', (chunk: Buffer | Uint8Array) => {
143
+ controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
144
+ });
145
+ stream.on('end', () => {
146
+ controller.close();
147
+ });
148
+ stream.on('error', (err: Error) => {
149
+ controller.error(err);
150
+ });
151
+ },
152
+ cancel() {
153
+ stream.destroy();
154
+ }
155
+ });
156
+ }
157
+
158
+ return null;
119
159
  }
120
160
 
121
161
  get _stream() {
@@ -128,18 +168,16 @@ export default class Body implements globalThis.Body {
128
168
 
129
169
  /**
130
170
  * Decode response as ArrayBuffer
131
- *
132
- * @return Promise
133
171
  */
134
- async arrayBuffer() {
172
+ async arrayBuffer(): Promise<ArrayBuffer> {
135
173
  const {buffer, byteOffset, byteLength} = await consumeBody(this);
136
- return buffer.slice(byteOffset, byteOffset + byteLength);
174
+ return buffer.slice(byteOffset, byteOffset + byteLength) as ArrayBuffer;
137
175
  }
138
176
 
139
- async formData() {
177
+ async formData(): Promise<FormData> {
140
178
  const ct = (this as unknown as Request).headers?.get('content-type');
141
179
 
142
- if (ct.startsWith('application/x-www-form-urlencoded')) {
180
+ if (ct?.startsWith('application/x-www-form-urlencoded')) {
143
181
  const formData = new FormData();
144
182
  const parameters = new URLSearchParams(await this.text());
145
183
 
@@ -156,11 +194,8 @@ export default class Body implements globalThis.Body {
156
194
 
157
195
  /**
158
196
  * Return raw response as Blob
159
- *
160
- * @return Promise
161
197
  */
162
- // @ts-ignore
163
- async blob() {
198
+ async blob(): Promise<Blob> {
164
199
  const ct = ((this as unknown as Request).headers?.get('content-type')) || (this[INTERNALS].body && (this[INTERNALS].body as Blob).type) || '';
165
200
  const buf = await this.arrayBuffer();
166
201
 
@@ -171,25 +206,21 @@ export default class Body implements globalThis.Body {
171
206
 
172
207
  /**
173
208
  * Decode response as json
174
- *
175
- * @return Promise
176
209
  */
177
- async json() {
210
+ async json(): Promise<unknown> {
178
211
  const text = await this.text();
179
212
  return JSON.parse(text);
180
213
  }
181
214
 
182
215
  /**
183
216
  * Decode response as text
184
- *
185
- * @return Promise
186
217
  */
187
- async text() {
218
+ async text(): Promise<string> {
188
219
  const buffer = await consumeBody(this);
189
220
  return new TextDecoder().decode(buffer);
190
221
  }
191
222
  }
192
-
223
+
193
224
  // In browsers, all properties are enumerable.
194
225
  Object.defineProperties(Body.prototype, {
195
226
  body: {enumerable: true},
@@ -198,19 +229,12 @@ Object.defineProperties(Body.prototype, {
198
229
  blob: {enumerable: true},
199
230
  json: {enumerable: true},
200
231
  text: {enumerable: true},
201
- data: {get: deprecate(() => {},
202
- 'data doesn\'t exist, use json(), text(), arrayBuffer(), or body instead',
203
- 'https://github.com/node-fetch/node-fetch/issues/1000 (response)')}
204
- });
205
-
206
- /**
207
- * Consume and convert an entire Body to a Buffer.
208
- *
209
- * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
210
- *
211
- * @return Promise
212
- */
213
- async function consumeBody(data: Body & Partial<Request>) {
232
+ });
233
+
234
+ /**
235
+ * Consume and convert an entire Body to a Buffer.
236
+ */
237
+ async function consumeBody(data: Body & { url?: string }): Promise<Buffer> {
214
238
  if (data[INTERNALS].disturbed) {
215
239
  throw new TypeError(`body used already for: ${data.url}`);
216
240
  }
@@ -228,14 +252,12 @@ async function consumeBody(data: Body & Partial<Request>) {
228
252
  return Buffer.alloc(0);
229
253
  }
230
254
 
231
- /* c8 ignore next 3 */
232
255
  if (!(body instanceof Stream)) {
233
256
  return Buffer.alloc(0);
234
257
  }
235
258
 
236
- // Body is stream
237
- // get ready to actually consume the body
238
- const accum = [];
259
+ // Body is stream — consume it
260
+ const accum: (Buffer | Uint8Array)[] = [];
239
261
  let accumBytes = 0;
240
262
 
241
263
  try {
@@ -249,97 +271,69 @@ async function consumeBody(data: Body & Partial<Request>) {
249
271
  accumBytes += chunk.length;
250
272
  accum.push(chunk);
251
273
  }
252
- } catch (error) {
253
- const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
274
+ } catch (error: unknown) {
275
+ const err = error instanceof Error ? error : new Error(String(error));
276
+ const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err as unknown as SystemError);
254
277
  throw error_;
255
278
  }
256
279
 
257
- if (body.readableEnded === true || (body as any)._readableState.ended === true) {
258
- try {
259
- if (accum.every(c => typeof c === 'string')) {
260
- return Buffer.from(accum.join(''));
261
- }
262
-
263
- return Buffer.concat(accum, accumBytes);
264
- } catch (error) {
265
- throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
280
+ try {
281
+ if (accum.every(c => typeof c === 'string')) {
282
+ return Buffer.from((accum as unknown as string[]).join(''));
266
283
  }
267
- } else {
268
- throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
284
+
285
+ return Buffer.concat(accum as Buffer[], accumBytes);
286
+ } catch (error: unknown) {
287
+ const err = error instanceof Error ? error : new Error(String(error));
288
+ throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${err.message}`, 'system', err as unknown as SystemError);
269
289
  }
270
290
  }
271
-
272
- /**
273
- * Clone body given Res/Req instance
274
- *
275
- * @param Mixed instance Response or Request instance
276
- * @param highWaterMark highWaterMark for both PassThrough body streams
277
- * @return Mixed
278
- */
291
+
292
+ /**
293
+ * Clone body given Res/Req instance
294
+ */
279
295
  export const clone = <T extends Request | Response>(instance: T, highWaterMark?: number) => {
280
296
  let p1: PassThrough;
281
297
  let p2: PassThrough;
282
298
  let {body} = instance[INTERNALS];
283
299
 
284
- // Don't allow cloning a used body
285
300
  if (instance.bodyUsed) {
286
301
  throw new Error('cannot clone body after it is used');
287
302
  }
288
303
 
289
- // Check that body is a stream and not form-data object
290
- // note: we can't clone the form-data object without having it as a dependency
291
- if ((body instanceof Stream) && (typeof (body as any).getBoundary !== 'function')) {
292
- // Tee instance body
304
+ if ((body instanceof Stream) && (typeof (body as unknown as Record<string, unknown>).getBoundary !== 'function')) {
293
305
  p1 = new PassThrough({highWaterMark});
294
306
  p2 = new PassThrough({highWaterMark});
295
307
  body.pipe(p1);
296
308
  body.pipe(p2);
297
- // Set instance body to teed body and return the other teed body
298
309
  instance[INTERNALS].stream = p1;
299
310
  body = p2;
300
311
  }
301
-
312
+
302
313
  return body;
303
- };
304
-
305
- // const getNonSpecFormDataBoundary = deprecate(
306
- // (body) => body.getBoundary(),
307
- // 'form-data doesn\'t follow the spec and requires special treatment. Use alternative package',
308
- // 'https://github.com/node-fetch/node-fetch/issues/1167'
309
- // );
310
-
311
- /**
312
- * Performs the operation "extract a `Content-Type` value from |object|" as
313
- * specified in the specification:
314
- * https://fetch.spec.whatwg.org/#concept-bodyinit-extract
315
- *
316
- * This function assumes that instance.body is present.
317
- *
318
- * @param body Any options.body input
319
- */
320
- export const extractContentType = (body: BodyInit | string | ArrayBuffer | Readable | Blob | ArrayBufferView | Buffer | FormData | globalThis.ReadableStream<any> | null, request: Request | Response): string | null => {
321
- // Body is null or undefined
314
+ };
315
+
316
+ /**
317
+ * Extract a Content-Type value from a body.
318
+ */
319
+ export const extractContentType = (body: BodyInit | Readable | Blob | Buffer | null, request: Request | Response): string | null => {
322
320
  if (body === null) {
323
321
  return null;
324
322
  }
325
323
 
326
- // Body is string
327
324
  if (typeof body === 'string') {
328
325
  return 'text/plain;charset=UTF-8';
329
326
  }
330
327
 
331
- // Body is a URLSearchParams
332
328
  if (isURLSearchParameters(body)) {
333
329
  return 'application/x-www-form-urlencoded;charset=UTF-8';
334
330
  }
335
331
 
336
- // Body is blob
337
332
  if (isBlob(body)) {
338
333
  return (body as Blob & globalThis.Blob).type || null;
339
334
  }
340
335
 
341
- // Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
342
- if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
336
+ if (Buffer.isBuffer(body) || isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
343
337
  return null;
344
338
  }
345
339
 
@@ -347,69 +341,87 @@ export const extractContentType = (body: BodyInit | string | ArrayBuffer | Reada
347
341
  return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
348
342
  }
349
343
 
350
- // Detect form data input from form-data module
351
- // if (body && typeof body.getBoundary === 'function') {
352
- // return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`;
353
- // }
354
-
355
- // Body is stream - can't really do much about this
356
344
  if (body instanceof Stream) {
357
345
  return null;
358
346
  }
359
347
 
360
- // Body constructor defaults other things to string
361
348
  return 'text/plain;charset=UTF-8';
362
349
  };
363
-
364
- /**
365
- * The Fetch Standard treats this as if "total bytes" is a property on the body.
366
- * For us, we have to explicitly get it with a function.
367
- *
368
- * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
369
- *
370
- * @param request Request object with the body property.
371
- */
350
+
351
+ /**
352
+ * Get total bytes of a body.
353
+ */
372
354
  export const getTotalBytes = (request: Request): number | null => {
373
355
  const { body } = request[INTERNALS];
374
356
 
375
- // Body is null or undefined
376
357
  if (body === null) {
377
358
  return 0;
378
359
  }
379
360
 
380
- // Body is Blob
381
361
  if (isBlob(body)) {
382
362
  return (body as Blob).size;
383
363
  }
384
364
 
385
- // Body is Buffer
386
365
  if (Buffer.isBuffer(body)) {
387
366
  return body.length;
388
367
  }
389
368
 
390
- // Detect form data input from form-data module
391
- if (body && typeof (body as any).getLengthSync === 'function') {
392
- const anyBody = body as any;
393
- return anyBody.hasKnownLength && anyBody.hasKnownLength() ? anyBody.getLengthSync() : null;
369
+ if (body && typeof (body as unknown as Record<string, unknown>).getLengthSync === 'function') {
370
+ const streamBody = body as unknown as { getLengthSync(): number; hasKnownLength?(): boolean };
371
+ return streamBody.hasKnownLength && streamBody.hasKnownLength() ? streamBody.getLengthSync() : null;
394
372
  }
395
373
 
396
- // Body is stream
397
374
  return null;
398
375
  };
399
-
400
- /**
401
- * Write a Body to a Node.js WritableStream (e.g. http.Request) object.
402
- *
403
- * @param dest The stream to write to.
404
- * @param obj.body Body object from the Body instance.
405
- */
406
- export const writeToStream = async (dest: Writable, {body}): Promise<void> => {
376
+
377
+ /**
378
+ * Write a Body to a Node.js WritableStream.
379
+ */
380
+ export const writeToStream = async (dest: Writable, {body}: {body: Readable | null}): Promise<void> => {
407
381
  if (body === null) {
408
- // Body is null
409
382
  dest.end();
410
383
  } else {
411
- // Body is stream
412
384
  await pipeline(body, dest);
413
385
  }
414
- };
415
-
386
+ };
387
+
388
+ /**
389
+ * Convert a Web ReadableStream to a Node.js Readable.
390
+ */
391
+ function readableStreamToReadable(webStream: ReadableStream): Readable {
392
+ const reader = webStream.getReader();
393
+ return new Readable({
394
+ async read() {
395
+ try {
396
+ const { done, value } = await reader.read();
397
+ if (done) {
398
+ this.push(null);
399
+ } else {
400
+ this.push(Buffer.from(value));
401
+ }
402
+ } catch (err) {
403
+ this.destroy(err as Error);
404
+ }
405
+ },
406
+ destroy(_err, callback) {
407
+ reader.cancel().then(() => callback(null), callback);
408
+ }
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Convert a Blob to an async iterable for Readable.from().
414
+ */
415
+ async function* blobToAsyncIterable(blob: Blob): AsyncIterable<Uint8Array> {
416
+ if (typeof blob.stream === 'function') {
417
+ const reader = (blob.stream() as unknown as ReadableStream).getReader();
418
+ while (true) {
419
+ const { done, value } = await reader.read();
420
+ if (done) break;
421
+ yield value;
422
+ }
423
+ } else {
424
+ // Fallback: read the entire blob at once
425
+ yield new Uint8Array(await blob.arrayBuffer());
426
+ }
427
+ }
@@ -5,7 +5,9 @@ export class FetchBaseError extends Error {
5
5
  constructor(message: string, type?: string) {
6
6
  super(message);
7
7
  // Hide custom error implementation details from end-users
8
- Error.captureStackTrace(this, this.constructor);
8
+ if (typeof Error.captureStackTrace === 'function') {
9
+ Error.captureStackTrace(this, this.constructor);
10
+ }
9
11
 
10
12
  this.type = type;
11
13
  }