@whatwg-node/node-fetch 0.8.0-alpha-20241212154840-0a0effe808a6614e0a3afd853126a38641485756 → 0.8.0-alpha-20250917012053-36c9ccdc3e94ee8d0961f17398a9053fa55df37b

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 (73) hide show
  1. package/cjs/AbortError.js +5 -3
  2. package/cjs/Blob.js +21 -19
  3. package/cjs/Body.js +146 -76
  4. package/cjs/FormData.js +54 -41
  5. package/cjs/Headers.js +54 -15
  6. package/cjs/ReadableStream.js +62 -26
  7. package/cjs/Request.js +9 -16
  8. package/cjs/Response.js +56 -10
  9. package/cjs/TextEncoderDecoder.js +6 -5
  10. package/cjs/TextEncoderDecoderStream.js +2 -6
  11. package/cjs/TransformStream.js +2 -1
  12. package/cjs/URL.js +10 -66
  13. package/cjs/URLSearchParams.js +1 -117
  14. package/cjs/WritableStream.js +35 -111
  15. package/cjs/fetch.js +37 -8
  16. package/cjs/fetchCurl.js +30 -61
  17. package/cjs/fetchNodeHttp.js +60 -64
  18. package/cjs/index.js +1 -7
  19. package/cjs/utils.js +76 -55
  20. package/esm/AbortError.js +5 -3
  21. package/esm/Blob.js +6 -4
  22. package/esm/Body.js +134 -63
  23. package/esm/FormData.js +54 -41
  24. package/esm/Headers.js +54 -15
  25. package/esm/ReadableStream.js +57 -21
  26. package/esm/Request.js +7 -14
  27. package/esm/Response.js +55 -9
  28. package/esm/TextEncoderDecoder.js +1 -0
  29. package/esm/TextEncoderDecoderStream.js +2 -6
  30. package/esm/TransformStream.js +2 -1
  31. package/esm/URL.js +9 -64
  32. package/esm/URLSearchParams.js +1 -115
  33. package/esm/WritableStream.js +33 -109
  34. package/esm/fetch.js +35 -6
  35. package/esm/fetchCurl.js +28 -59
  36. package/esm/fetchNodeHttp.js +55 -59
  37. package/esm/index.js +0 -3
  38. package/esm/utils.js +70 -53
  39. package/package.json +4 -5
  40. package/typings/AbortError.d.cts +1 -1
  41. package/typings/AbortError.d.ts +1 -1
  42. package/typings/Blob.d.cts +5 -4
  43. package/typings/Blob.d.ts +5 -4
  44. package/typings/Body.d.cts +11 -6
  45. package/typings/Body.d.ts +11 -6
  46. package/typings/Headers.d.cts +1 -1
  47. package/typings/Headers.d.ts +1 -1
  48. package/typings/ReadableStream.d.cts +8 -2
  49. package/typings/ReadableStream.d.ts +8 -2
  50. package/typings/Request.d.cts +9 -10
  51. package/typings/Request.d.ts +9 -10
  52. package/typings/Response.d.cts +6 -5
  53. package/typings/Response.d.ts +6 -5
  54. package/typings/TextEncoderDecoder.d.cts +2 -1
  55. package/typings/TextEncoderDecoder.d.ts +2 -1
  56. package/typings/URL.d.cts +12 -16
  57. package/typings/URL.d.ts +12 -16
  58. package/typings/URLSearchParams.d.cts +4 -21
  59. package/typings/URLSearchParams.d.ts +4 -21
  60. package/typings/WritableStream.d.cts +1 -1
  61. package/typings/WritableStream.d.ts +1 -1
  62. package/typings/index.d.cts +0 -3
  63. package/typings/index.d.ts +0 -3
  64. package/typings/utils.d.cts +13 -8
  65. package/typings/utils.d.ts +13 -8
  66. package/cjs/AbortController.js +0 -18
  67. package/cjs/AbortSignal.js +0 -85
  68. package/esm/AbortController.js +0 -14
  69. package/esm/AbortSignal.js +0 -81
  70. package/typings/AbortController.d.cts +0 -8
  71. package/typings/AbortController.d.ts +0 -8
  72. package/typings/AbortSignal.d.cts +0 -15
  73. package/typings/AbortSignal.d.ts +0 -15
package/esm/Body.js CHANGED
@@ -1,5 +1,8 @@
1
- import { Readable } from 'stream';
2
- import busboy from 'busboy';
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { Buffer } from 'node:buffer';
3
+ import { addAbortSignal, Readable } from 'node:stream';
4
+ import { Busboy } from '@fastify/busboy';
5
+ import { handleMaybePromise } from '@whatwg-node/promise-helpers';
3
6
  import { hasArrayBufferMethod, hasBufferMethod, hasBytesMethod, PonyfillBlob } from './Blob.js';
4
7
  import { PonyfillFile } from './File.js';
5
8
  import { getStreamFromFormData, PonyfillFormData } from './FormData.js';
@@ -30,11 +33,13 @@ export class PonyfillBody {
30
33
  this.contentLength = contentLength;
31
34
  this.bodyType = bodyType;
32
35
  this._buffer = buffer;
36
+ this._signal = options.signal;
33
37
  }
34
38
  bodyType;
35
39
  _bodyFactory = () => null;
36
40
  _generatedBody = null;
37
41
  _buffer;
42
+ _signal;
38
43
  generateBody() {
39
44
  if (this._generatedBody?.readable?.destroyed && this._buffer) {
40
45
  this._generatedBody.readable = Readable.from(this._buffer);
@@ -97,48 +102,66 @@ export class PonyfillBody {
97
102
  return null;
98
103
  }
99
104
  _chunks = null;
100
- _collectChunksFromReadable() {
101
- if (this._chunks) {
102
- return fakePromise(this._chunks);
103
- }
105
+ _doCollectChunksFromReadableJob() {
104
106
  if (this.bodyType === BodyInitType.AsyncIterable) {
105
- const iterator = this.bodyInit[Symbol.asyncIterator]();
106
- const collectValue = () => {
107
- return iterator.next().then(({ value, done }) => {
108
- this._chunks ||= [];
109
- if (value) {
110
- this._chunks.push(value);
111
- }
112
- if (!done) {
113
- return collectValue();
114
- }
107
+ if (Array.fromAsync) {
108
+ return handleMaybePromise(() => Array.fromAsync(this.bodyInit), chunks => {
109
+ this._chunks = chunks;
115
110
  return this._chunks;
116
111
  });
117
- };
112
+ }
113
+ const iterator = this.bodyInit[Symbol.asyncIterator]();
114
+ const chunks = [];
115
+ const collectValue = () => handleMaybePromise(() => iterator.next(), ({ value, done }) => {
116
+ if (value) {
117
+ chunks.push(value);
118
+ }
119
+ if (!done) {
120
+ return collectValue();
121
+ }
122
+ this._chunks = chunks;
123
+ return this._chunks;
124
+ });
118
125
  return collectValue();
119
126
  }
120
127
  const _body = this.generateBody();
121
128
  if (!_body) {
122
- return fakePromise([]);
129
+ this._chunks = [];
130
+ return fakePromise(this._chunks);
123
131
  }
124
- this._chunks = [];
125
- _body.readable.on('data', chunk => {
126
- this._chunks.push(chunk);
127
- });
132
+ if (_body.readable.destroyed) {
133
+ return fakePromise((this._chunks = []));
134
+ }
135
+ const chunks = [];
128
136
  return new Promise((resolve, reject) => {
129
- _body.readable.once('end', () => {
130
- resolve(this._chunks);
137
+ _body.readable.on('data', chunk => {
138
+ chunks.push(chunk);
131
139
  });
132
- _body.readable.once('error', e => {
133
- reject(e);
140
+ _body.readable.once('error', reject);
141
+ _body.readable.once('end', () => {
142
+ resolve((this._chunks = chunks));
134
143
  });
135
144
  });
136
145
  }
146
+ _collectChunksFromReadable() {
147
+ if (this._chunks) {
148
+ return fakePromise(this._chunks);
149
+ }
150
+ this._chunks ||= this._doCollectChunksFromReadableJob();
151
+ return this._chunks;
152
+ }
137
153
  _blob = null;
138
154
  blob() {
139
155
  if (this._blob) {
140
156
  return fakePromise(this._blob);
141
157
  }
158
+ if (this.bodyType === BodyInitType.String) {
159
+ this._text = this.bodyInit;
160
+ this._blob = new PonyfillBlob([this._text], {
161
+ type: this.contentType || 'text/plain;charset=UTF-8',
162
+ size: this.contentLength,
163
+ });
164
+ }
142
165
  if (this.bodyType === BodyInitType.Blob) {
143
166
  this._blob = this.bodyInit;
144
167
  return fakePromise(this._blob);
@@ -150,13 +173,13 @@ export class PonyfillBody {
150
173
  });
151
174
  return fakePromise(this._blob);
152
175
  }
153
- return this._collectChunksFromReadable().then(chunks => {
176
+ return fakePromise(handleMaybePromise(() => this._collectChunksFromReadable(), chunks => {
154
177
  this._blob = new PonyfillBlob(chunks, {
155
178
  type: this.contentType || '',
156
179
  size: this.contentLength,
157
180
  });
158
181
  return this._blob;
159
- });
182
+ }));
160
183
  }
161
184
  _formData = null;
162
185
  formData(opts) {
@@ -177,61 +200,108 @@ export class PonyfillBody {
177
200
  ...opts?.formDataLimits,
178
201
  };
179
202
  return new Promise((resolve, reject) => {
180
- const bb = busboy({
203
+ const stream = this.body?.readable;
204
+ if (!stream) {
205
+ return reject(new Error('No stream available'));
206
+ }
207
+ // form data file that is currently being processed, it's
208
+ // important to keep track of it in case the stream ends early
209
+ let currFile = null;
210
+ const bb = new Busboy({
181
211
  headers: {
212
+ 'content-length': typeof this.contentLength === 'number'
213
+ ? this.contentLength.toString()
214
+ : this.contentLength || '',
182
215
  'content-type': this.contentType || '',
183
216
  },
184
217
  limits: formDataLimits,
185
- defParamCharset: 'utf-8',
218
+ defCharset: 'utf-8',
186
219
  });
187
- bb.on('field', (name, value, { nameTruncated, valueTruncated }) => {
188
- if (nameTruncated) {
189
- reject(new Error(`Field name size exceeded: ${formDataLimits?.fieldNameSize} bytes`));
220
+ if (this._signal) {
221
+ addAbortSignal(this._signal, bb);
222
+ }
223
+ let completed = false;
224
+ const complete = (err) => {
225
+ if (completed)
226
+ return;
227
+ completed = true;
228
+ stream.unpipe(bb);
229
+ bb.destroy();
230
+ if (currFile) {
231
+ currFile.destroy();
232
+ currFile = null;
233
+ }
234
+ if (err) {
235
+ reject(err);
236
+ }
237
+ else {
238
+ // no error occured, this is a successful end/complete/finish
239
+ resolve(this._formData);
240
+ }
241
+ };
242
+ // we dont need to listen to the stream close event because bb will close or error when necessary
243
+ // stream.on('close', complete);
244
+ // stream can be aborted, for example
245
+ stream.on('error', complete);
246
+ bb.on('field', (name, value, fieldnameTruncated, valueTruncated) => {
247
+ if (fieldnameTruncated) {
248
+ return complete(new Error(`Field name size exceeded: ${formDataLimits?.fieldNameSize} bytes`));
190
249
  }
191
250
  if (valueTruncated) {
192
- reject(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
251
+ return complete(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
193
252
  }
194
253
  this._formData.set(name, value);
195
254
  });
196
- bb.on('fieldsLimit', () => {
197
- reject(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
198
- });
199
- bb.on('file', (name, fileStream, { filename, mimeType }) => {
255
+ bb.on('file', (name, fileStream, filename, _transferEncoding, mimeType) => {
256
+ currFile = fileStream;
200
257
  const chunks = [];
201
- fileStream.on('limit', () => {
202
- reject(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
203
- });
204
258
  fileStream.on('data', chunk => {
205
259
  chunks.push(chunk);
206
260
  });
261
+ fileStream.on('error', complete);
262
+ fileStream.on('limit', () => {
263
+ complete(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
264
+ });
207
265
  fileStream.on('close', () => {
208
266
  if (fileStream.truncated) {
209
- reject(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
267
+ complete(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
210
268
  }
269
+ currFile = null;
211
270
  const file = new PonyfillFile(chunks, filename, { type: mimeType });
212
271
  this._formData.set(name, file);
213
272
  });
214
273
  });
274
+ bb.on('fieldsLimit', () => {
275
+ complete(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
276
+ });
215
277
  bb.on('filesLimit', () => {
216
- reject(new Error(`Files limit exceeded: ${formDataLimits?.files}`));
278
+ complete(new Error(`Files limit exceeded: ${formDataLimits?.files}`));
217
279
  });
218
280
  bb.on('partsLimit', () => {
219
- reject(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
281
+ complete(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
220
282
  });
221
- bb.on('close', () => {
222
- resolve(this._formData);
223
- });
224
- bb.on('error', (err = 'An error occurred while parsing the form data') => {
225
- const errMessage = err.message || err.toString();
226
- reject(new TypeError(errMessage, err.cause));
227
- });
228
- _body?.readable.pipe(bb);
283
+ bb.on('end', complete);
284
+ bb.on('finish', complete);
285
+ bb.on('close', complete);
286
+ bb.on('error', complete);
287
+ stream.pipe(bb);
229
288
  });
230
289
  }
231
290
  buffer() {
232
291
  if (this._buffer) {
233
292
  return fakePromise(this._buffer);
234
293
  }
294
+ if (this._text) {
295
+ this._buffer = Buffer.from(this._text, 'utf-8');
296
+ return fakePromise(this._buffer);
297
+ }
298
+ if (this.bodyType === BodyInitType.String) {
299
+ return this.text().then(text => {
300
+ this._text = text;
301
+ this._buffer = Buffer.from(text, 'utf-8');
302
+ return this._buffer;
303
+ });
304
+ }
235
305
  if (this.bodyType === BodyInitType.Blob) {
236
306
  if (hasBufferMethod(this.bodyInit)) {
237
307
  return this.bodyInit.buffer().then(buf => {
@@ -252,20 +322,20 @@ export class PonyfillBody {
252
322
  });
253
323
  }
254
324
  }
255
- return this._collectChunksFromReadable().then(chunks => {
325
+ return fakePromise(handleMaybePromise(() => this._collectChunksFromReadable(), chunks => {
256
326
  if (chunks.length === 1) {
257
327
  this._buffer = chunks[0];
258
328
  return this._buffer;
259
329
  }
260
330
  this._buffer = Buffer.concat(chunks);
261
331
  return this._buffer;
262
- });
332
+ }));
263
333
  }
264
334
  bytes() {
265
335
  return this.buffer();
266
336
  }
267
337
  arrayBuffer() {
268
- // @ts-expect-error - `Buffer` and `ArrayBuffer` are compatible
338
+ // @ts-ignore - Mismatch between Buffer and ArrayBuffer
269
339
  return this.buffer();
270
340
  }
271
341
  _json = null;
@@ -310,27 +380,26 @@ function processBodyInit(bodyInit) {
310
380
  };
311
381
  }
312
382
  if (typeof bodyInit === 'string') {
313
- const buffer = Buffer.from(bodyInit);
314
- const contentLength = buffer.byteLength;
383
+ const contentLength = Buffer.byteLength(bodyInit);
315
384
  return {
316
385
  bodyType: BodyInitType.String,
317
386
  contentType: 'text/plain;charset=UTF-8',
318
387
  contentLength,
319
- buffer,
320
388
  bodyFactory() {
321
- const readable = Readable.from(buffer);
389
+ const readable = Readable.from(Buffer.from(bodyInit, 'utf-8'));
322
390
  return new PonyfillReadableStream(readable);
323
391
  },
324
392
  };
325
393
  }
326
394
  if (Buffer.isBuffer(bodyInit)) {
395
+ const buffer = bodyInit;
327
396
  return {
328
397
  bodyType: BodyInitType.Buffer,
329
398
  contentType: null,
330
399
  contentLength: bodyInit.length,
331
400
  buffer: bodyInit,
332
401
  bodyFactory() {
333
- const readable = Readable.from(bodyInit);
402
+ const readable = Readable.from(buffer);
334
403
  const body = new PonyfillReadableStream(readable);
335
404
  return body;
336
405
  },
@@ -351,20 +420,22 @@ function processBodyInit(bodyInit) {
351
420
  };
352
421
  }
353
422
  if (bodyInit instanceof PonyfillReadableStream && bodyInit.readable != null) {
423
+ const readableStream = bodyInit;
354
424
  return {
355
425
  bodyType: BodyInitType.ReadableStream,
356
- bodyFactory: () => bodyInit,
426
+ bodyFactory: () => readableStream,
357
427
  contentType: null,
358
428
  contentLength: null,
359
429
  };
360
430
  }
361
431
  if (isBlob(bodyInit)) {
432
+ const blob = bodyInit;
362
433
  return {
363
434
  bodyType: BodyInitType.Blob,
364
435
  contentType: bodyInit.type,
365
436
  contentLength: bodyInit.size,
366
437
  bodyFactory() {
367
- return bodyInit.stream();
438
+ return blob.stream();
368
439
  },
369
440
  };
370
441
  }
@@ -444,7 +515,7 @@ function isFormData(value) {
444
515
  return value?.forEach != null;
445
516
  }
446
517
  function isBlob(value) {
447
- return value?.stream != null;
518
+ return value?.stream != null && typeof value.stream === 'function';
448
519
  }
449
520
  function isURLSearchParams(value) {
450
521
  return value?.sort != null;
package/esm/FormData.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { PonyfillIteratorObject } from './IteratorObject.js';
2
3
  import { PonyfillReadableStream } from './ReadableStream.js';
3
4
  export class PonyfillFormData {
@@ -68,54 +69,66 @@ export class PonyfillFormData {
68
69
  }
69
70
  }
70
71
  export function getStreamFromFormData(formData, boundary = '---') {
71
- const entries = [];
72
+ let entriesIterator;
72
73
  let sentInitialHeader = false;
73
- return new PonyfillReadableStream({
74
- start: controller => {
75
- formData.forEach((value, key) => {
76
- if (!sentInitialHeader) {
77
- controller.enqueue(Buffer.from(`--${boundary}\r\n`));
78
- sentInitialHeader = true;
74
+ let currentAsyncIterator;
75
+ let hasBefore = false;
76
+ function handleNextEntry(controller) {
77
+ const { done, value } = entriesIterator.next();
78
+ if (done) {
79
+ controller.enqueue(Buffer.from(`\r\n--${boundary}--\r\n`));
80
+ return controller.close();
81
+ }
82
+ if (hasBefore) {
83
+ controller.enqueue(Buffer.from(`\r\n--${boundary}\r\n`));
84
+ }
85
+ if (value) {
86
+ const [key, blobOrString] = value;
87
+ if (typeof blobOrString === 'string') {
88
+ controller.enqueue(Buffer.from(`Content-Disposition: form-data; name="${key}"\r\n\r\n`));
89
+ controller.enqueue(Buffer.from(blobOrString));
90
+ }
91
+ else {
92
+ let filenamePart = '';
93
+ if (blobOrString.name) {
94
+ filenamePart = `; filename="${blobOrString.name}"`;
79
95
  }
80
- entries.push([key, value]);
81
- });
82
- if (!sentInitialHeader) {
83
- controller.enqueue(Buffer.from(`--${boundary}--\r\n`));
84
- controller.close();
96
+ controller.enqueue(Buffer.from(`Content-Disposition: form-data; name="${key}"${filenamePart}\r\n`));
97
+ controller.enqueue(Buffer.from(`Content-Type: ${blobOrString.type || 'application/octet-stream'}\r\n\r\n`));
98
+ const entryStream = blobOrString.stream();
99
+ // @ts-expect-error - ReadableStream is async iterable
100
+ currentAsyncIterator = entryStream[Symbol.asyncIterator]();
85
101
  }
102
+ hasBefore = true;
103
+ }
104
+ }
105
+ return new PonyfillReadableStream({
106
+ start: () => {
107
+ entriesIterator = formData.entries();
86
108
  },
87
- pull: async (controller) => {
88
- const entry = entries.shift();
89
- if (entry) {
90
- const [key, value] = entry;
91
- if (typeof value === 'string') {
92
- controller.enqueue(Buffer.from(`Content-Disposition: form-data; name="${key}"\r\n\r\n`));
93
- controller.enqueue(Buffer.from(value));
94
- }
95
- else {
96
- let filenamePart = '';
97
- if (value.name) {
98
- filenamePart = `; filename="${value.name}"`;
109
+ pull: controller => {
110
+ if (!sentInitialHeader) {
111
+ sentInitialHeader = true;
112
+ return controller.enqueue(Buffer.from(`--${boundary}\r\n`));
113
+ }
114
+ if (currentAsyncIterator) {
115
+ return currentAsyncIterator.next().then(({ done, value }) => {
116
+ if (done) {
117
+ currentAsyncIterator = undefined;
99
118
  }
100
- controller.enqueue(Buffer.from(`Content-Disposition: form-data; name="${key}"${filenamePart}\r\n`));
101
- controller.enqueue(Buffer.from(`Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`));
102
- const entryStream = value.stream();
103
- for await (const chunk of entryStream) {
104
- controller.enqueue(chunk);
119
+ if (value) {
120
+ return controller.enqueue(value);
105
121
  }
106
- }
107
- if (entries.length === 0) {
108
- controller.enqueue(Buffer.from(`\r\n--${boundary}--\r\n`));
109
- controller.close();
110
- }
111
- else {
112
- controller.enqueue(Buffer.from(`\r\n--${boundary}\r\n`));
113
- }
114
- }
115
- else {
116
- controller.enqueue(Buffer.from(`\r\n--${boundary}--\r\n`));
117
- controller.close();
122
+ else {
123
+ return handleNextEntry(controller);
124
+ }
125
+ });
118
126
  }
127
+ return handleNextEntry(controller);
128
+ },
129
+ cancel: err => {
130
+ entriesIterator?.return?.(err);
131
+ currentAsyncIterator?.return?.(err);
119
132
  },
120
133
  });
121
134
  }
package/esm/Headers.js CHANGED
@@ -8,14 +8,14 @@ export class PonyfillHeaders {
8
8
  _map;
9
9
  objectNormalizedKeysOfHeadersInit = [];
10
10
  objectOriginalKeysOfHeadersInit = [];
11
- _setCookies = [];
11
+ _setCookies;
12
12
  constructor(headersInit) {
13
13
  this.headersInit = headersInit;
14
14
  }
15
15
  // perf: we don't need to build `this.map` for Requests, as we can access the headers directly
16
16
  _get(key) {
17
17
  const normalized = key.toLowerCase();
18
- if (normalized === 'set-cookie') {
18
+ if (normalized === 'set-cookie' && this._setCookies?.length) {
19
19
  return this._setCookies.join(', ');
20
20
  }
21
21
  // If the map is built, reuse it
@@ -27,7 +27,14 @@ export class PonyfillHeaders {
27
27
  return null;
28
28
  }
29
29
  if (Array.isArray(this.headersInit)) {
30
- return this.headersInit.find(header => header[0].toLowerCase() === normalized)?.[1] || null;
30
+ const found = this.headersInit.filter(([headerKey]) => headerKey.toLowerCase() === normalized);
31
+ if (found.length === 0) {
32
+ return null;
33
+ }
34
+ if (found.length === 1) {
35
+ return found[0][1];
36
+ }
37
+ return found.map(([, value]) => value).join(', ');
31
38
  }
32
39
  else if (isHeadersLike(this.headersInit)) {
33
40
  return this.headersInit.get(normalized);
@@ -55,22 +62,24 @@ export class PonyfillHeaders {
55
62
  // I could do a getter here, but I'm too lazy to type `getter`.
56
63
  getMap() {
57
64
  if (!this._map) {
65
+ this._setCookies ||= [];
58
66
  if (this.headersInit != null) {
59
67
  if (Array.isArray(this.headersInit)) {
60
68
  this._map = new Map();
61
- this.headersInit.forEach(([key, value]) => {
69
+ for (const [key, value] of this.headersInit) {
62
70
  const normalizedKey = key.toLowerCase();
63
71
  if (normalizedKey === 'set-cookie') {
64
72
  this._setCookies.push(value);
65
- return;
73
+ continue;
66
74
  }
67
75
  this._map.set(normalizedKey, value);
68
- });
76
+ }
69
77
  }
70
78
  else if (isHeadersLike(this.headersInit)) {
71
79
  this._map = new Map();
72
80
  this.headersInit.forEach((value, key) => {
73
81
  if (key === 'set-cookie') {
82
+ this._setCookies ||= [];
74
83
  this._setCookies.push(value);
75
84
  return;
76
85
  }
@@ -84,6 +93,7 @@ export class PonyfillHeaders {
84
93
  if (initValue != null) {
85
94
  const normalizedKey = initKey.toLowerCase();
86
95
  if (normalizedKey === 'set-cookie') {
96
+ this._setCookies ||= [];
87
97
  this._setCookies.push(initValue);
88
98
  continue;
89
99
  }
@@ -101,6 +111,7 @@ export class PonyfillHeaders {
101
111
  append(name, value) {
102
112
  const key = name.toLowerCase();
103
113
  if (key === 'set-cookie') {
114
+ this._setCookies ||= [];
104
115
  this._setCookies.push(value);
105
116
  return;
106
117
  }
@@ -113,11 +124,12 @@ export class PonyfillHeaders {
113
124
  if (value == null) {
114
125
  return null;
115
126
  }
116
- return value;
127
+ return value.toString();
117
128
  }
118
129
  has(name) {
119
- if (name === 'set-cookie') {
120
- return this._setCookies.length > 0;
130
+ const key = name.toLowerCase();
131
+ if (key === 'set-cookie') {
132
+ return !!this._setCookies?.length;
121
133
  }
122
134
  return !!this._get(name); // we might need to check if header exists and not just check if it's not nullable
123
135
  }
@@ -127,6 +139,26 @@ export class PonyfillHeaders {
127
139
  this._setCookies = [value];
128
140
  return;
129
141
  }
142
+ if (!this._map && this.headersInit != null) {
143
+ if (Array.isArray(this.headersInit)) {
144
+ const found = this.headersInit.find(([headerKey]) => headerKey.toLowerCase() === key);
145
+ if (found) {
146
+ found[1] = value;
147
+ }
148
+ else {
149
+ this.headersInit.push([key, value]);
150
+ }
151
+ return;
152
+ }
153
+ else if (isHeadersLike(this.headersInit)) {
154
+ this.headersInit.set(key, value);
155
+ return;
156
+ }
157
+ else {
158
+ this.headersInit[key] = value;
159
+ return;
160
+ }
161
+ }
130
162
  this.getMap().set(key, value);
131
163
  }
132
164
  delete(name) {
@@ -138,7 +170,7 @@ export class PonyfillHeaders {
138
170
  this.getMap().delete(key);
139
171
  }
140
172
  forEach(callback) {
141
- this._setCookies.forEach(setCookie => {
173
+ this._setCookies?.forEach(setCookie => {
142
174
  callback(setCookie, 'set-cookie', this);
143
175
  });
144
176
  if (!this._map) {
@@ -166,7 +198,7 @@ export class PonyfillHeaders {
166
198
  });
167
199
  }
168
200
  *_keys() {
169
- if (this._setCookies.length) {
201
+ if (this._setCookies?.length) {
170
202
  yield 'set-cookie';
171
203
  }
172
204
  if (!this._map) {
@@ -189,7 +221,9 @@ export class PonyfillHeaders {
189
221
  return new PonyfillIteratorObject(this._keys(), 'HeadersIterator');
190
222
  }
191
223
  *_values() {
192
- yield* this._setCookies;
224
+ if (this._setCookies?.length) {
225
+ yield* this._setCookies;
226
+ }
193
227
  if (!this._map) {
194
228
  if (this.headersInit) {
195
229
  if (Array.isArray(this.headersInit)) {
@@ -210,7 +244,9 @@ export class PonyfillHeaders {
210
244
  return new PonyfillIteratorObject(this._values(), 'HeadersIterator');
211
245
  }
212
246
  *_entries() {
213
- yield* this._setCookies.map(cookie => ['set-cookie', cookie]);
247
+ if (this._setCookies?.length) {
248
+ yield* this._setCookies.map(cookie => ['set-cookie', cookie]);
249
+ }
214
250
  if (!this._map) {
215
251
  if (this.headersInit) {
216
252
  if (Array.isArray(this.headersInit)) {
@@ -231,6 +267,9 @@ export class PonyfillHeaders {
231
267
  return new PonyfillIteratorObject(this._entries(), 'HeadersIterator');
232
268
  }
233
269
  getSetCookie() {
270
+ if (!this._setCookies) {
271
+ this.getMap();
272
+ }
234
273
  return this._setCookies;
235
274
  }
236
275
  [Symbol.iterator]() {
@@ -240,10 +279,10 @@ export class PonyfillHeaders {
240
279
  const record = {};
241
280
  this.forEach((value, key) => {
242
281
  if (key === 'set-cookie') {
243
- record['set-cookie'] = this._setCookies;
282
+ record['set-cookie'] = this._setCookies || [];
244
283
  }
245
284
  else {
246
- record[key] = value.includes(',') ? value.split(',').map(el => el.trim()) : value;
285
+ record[key] = value?.includes(',') ? value.split(',').map(el => el.trim()) : value;
247
286
  }
248
287
  });
249
288
  return `Headers ${inspect(record)}`;