@whatwg-node/node-fetch 0.7.22-alpha-20250527020114-bf7a9c1c4bf32e1ca6edd3faf2d7a81370ded65c → 0.7.22-alpha-20250710160716-6df7209d12299dba404ac539d834654be5d43990

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/cjs/Body.js CHANGED
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PonyfillBody = void 0;
4
4
  /* eslint-disable @typescript-eslint/ban-ts-comment */
5
5
  const node_buffer_1 = require("node:buffer");
6
+ const node_http_1 = require("node:http");
6
7
  const node_stream_1 = require("node:stream");
7
- const promises_1 = require("node:stream/promises");
8
8
  const busboy_1 = require("@fastify/busboy");
9
9
  const promise_helpers_1 = require("@whatwg-node/promise-helpers");
10
10
  const Blob_js_1 = require("./Blob.js");
@@ -33,7 +33,7 @@ class PonyfillBody {
33
33
  this.bodyInit = bodyInit;
34
34
  this.options = options;
35
35
  this._signal = options.signal || null;
36
- const { bodyFactory, contentType, contentLength, bodyType, buffer } = processBodyInit(bodyInit);
36
+ const { bodyFactory, contentType, contentLength, bodyType, buffer } = processBodyInit(bodyInit, options?.signal);
37
37
  this._bodyFactory = bodyFactory;
38
38
  this.contentType = contentType;
39
39
  this.contentLength = contentLength;
@@ -109,14 +109,14 @@ class PonyfillBody {
109
109
  _doCollectChunksFromReadableJob() {
110
110
  if (this.bodyType === BodyInitType.AsyncIterable) {
111
111
  if (Array.fromAsync) {
112
- return Array.fromAsync(this.bodyInit).then(chunks => {
112
+ return (0, promise_helpers_1.handleMaybePromise)(() => Array.fromAsync(this.bodyInit), chunks => {
113
113
  this._chunks = chunks;
114
114
  return this._chunks;
115
115
  });
116
116
  }
117
117
  const iterator = this.bodyInit[Symbol.asyncIterator]();
118
118
  const chunks = [];
119
- const collectValue = () => iterator.next().then(({ value, done }) => {
119
+ const collectValue = () => (0, promise_helpers_1.handleMaybePromise)(() => iterator.next(), ({ value, done }) => {
120
120
  if (value) {
121
121
  chunks.push(value);
122
122
  }
@@ -131,30 +131,23 @@ class PonyfillBody {
131
131
  const _body = this.generateBody();
132
132
  if (!_body) {
133
133
  this._chunks = [];
134
- return this._chunks;
135
- }
136
- if (_body.readable.destroyed) {
137
- // If the stream is already destroyed, we can resolve immediately
138
- this._chunks = [];
139
- return this._chunks;
134
+ return (0, utils_js_1.fakePromise)(this._chunks);
140
135
  }
141
136
  const chunks = [];
142
- const deferred = (0, promise_helpers_1.createDeferredPromise)();
143
- _body.readable.on('data', function nextChunk(chunk) {
144
- chunks.push(chunk);
145
- });
146
- // eslint-disable-next-line @typescript-eslint/no-this-alias
147
- const _this = this;
148
- _body.readable.once('end', function endOfStream() {
149
- _this._chunks = chunks;
150
- deferred.resolve(chunks);
151
- });
152
- _body.readable.once('error', function errorOnStream(err) {
153
- deferred.reject(err);
137
+ return new Promise((resolve, reject) => {
138
+ _body.readable.on('data', chunk => {
139
+ chunks.push(chunk);
140
+ });
141
+ _body.readable.once('error', reject);
142
+ _body.readable.once('end', () => {
143
+ resolve((this._chunks = chunks));
144
+ });
154
145
  });
155
- return deferred.promise;
156
146
  }
157
147
  _collectChunksFromReadable() {
148
+ if (this._chunks) {
149
+ return (0, utils_js_1.fakePromise)(this._chunks);
150
+ }
158
151
  this._chunks ||= this._doCollectChunksFromReadableJob();
159
152
  return this._chunks;
160
153
  }
@@ -181,15 +174,13 @@ class PonyfillBody {
181
174
  });
182
175
  return (0, utils_js_1.fakePromise)(this._blob);
183
176
  }
184
- return (0, utils_js_1.fakePromise)()
185
- .then(() => this._collectChunksFromReadable())
186
- .then(chunks => {
177
+ return (0, utils_js_1.fakePromise)((0, promise_helpers_1.handleMaybePromise)(() => this._collectChunksFromReadable(), chunks => {
187
178
  this._blob = new Blob_js_1.PonyfillBlob(chunks, {
188
179
  type: this.contentType || '',
189
180
  size: this.contentLength,
190
181
  });
191
182
  return this._blob;
192
- });
183
+ }));
193
184
  }
194
185
  _formData = null;
195
186
  formData(opts) {
@@ -209,57 +200,93 @@ class PonyfillBody {
209
200
  ...this.options.formDataLimits,
210
201
  ...opts?.formDataLimits,
211
202
  };
212
- const stream = this.body?.readable;
213
- if (!stream) {
214
- return (0, promise_helpers_1.fakeRejectPromise)(new Error('No stream available'));
215
- }
216
- const bb = new busboy_1.Busboy({
217
- headers: {
218
- 'content-length': typeof this.contentLength === 'number'
219
- ? this.contentLength.toString()
220
- : this.contentLength || '',
221
- 'content-type': this.contentType || '',
222
- },
223
- limits: formDataLimits,
224
- defCharset: 'utf-8',
225
- });
226
- bb.on('field', (name, value, fieldnameTruncated, valueTruncated) => {
227
- if (fieldnameTruncated) {
228
- bb.destroy(new Error(`Field name size exceeded: ${formDataLimits?.fieldNameSize} bytes`));
203
+ return new Promise((resolve, reject) => {
204
+ const stream = this.body?.readable;
205
+ if (!stream) {
206
+ return reject(new Error('No stream available'));
229
207
  }
230
- if (valueTruncated) {
231
- bb.destroy(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
208
+ // form data file that is currently being processed, it's
209
+ // important to keep track of it in case the stream ends early
210
+ let currFile = null;
211
+ const bb = new busboy_1.Busboy({
212
+ headers: {
213
+ 'content-length': typeof this.contentLength === 'number'
214
+ ? this.contentLength.toString()
215
+ : this.contentLength || '',
216
+ 'content-type': this.contentType || '',
217
+ },
218
+ limits: formDataLimits,
219
+ defCharset: 'utf-8',
220
+ });
221
+ if (this._signal) {
222
+ (0, node_stream_1.addAbortSignal)(this._signal, bb);
232
223
  }
233
- this._formData.set(name, value);
234
- });
235
- bb.on('file', (name, fileStream, filename, _transferEncoding, mimeType) => {
236
- const chunks = [];
237
- fileStream.on('data', chunk => {
238
- chunks.push(chunk);
224
+ let completed = false;
225
+ const complete = (err) => {
226
+ if (completed)
227
+ return;
228
+ completed = true;
229
+ stream.unpipe(bb);
230
+ bb.destroy();
231
+ if (currFile) {
232
+ currFile.destroy();
233
+ currFile = null;
234
+ }
235
+ if (err) {
236
+ reject(err);
237
+ }
238
+ else {
239
+ // no error occured, this is a successful end/complete/finish
240
+ resolve(this._formData);
241
+ }
242
+ };
243
+ // we dont need to listen to the stream close event because bb will close or error when necessary
244
+ // stream.on('close', complete);
245
+ // stream can be aborted, for example
246
+ stream.on('error', complete);
247
+ bb.on('field', (name, value, fieldnameTruncated, valueTruncated) => {
248
+ if (fieldnameTruncated) {
249
+ return complete(new Error(`Field name size exceeded: ${formDataLimits?.fieldNameSize} bytes`));
250
+ }
251
+ if (valueTruncated) {
252
+ return complete(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
253
+ }
254
+ this._formData.set(name, value);
239
255
  });
240
- fileStream.on('limit', () => {
241
- bb.destroy(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
256
+ bb.on('file', (name, fileStream, filename, _transferEncoding, mimeType) => {
257
+ currFile = fileStream;
258
+ const chunks = [];
259
+ fileStream.on('data', chunk => {
260
+ chunks.push(chunk);
261
+ });
262
+ fileStream.on('error', complete);
263
+ fileStream.on('limit', () => {
264
+ complete(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
265
+ });
266
+ fileStream.on('close', () => {
267
+ if (fileStream.truncated) {
268
+ complete(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
269
+ }
270
+ currFile = null;
271
+ const file = new File_js_1.PonyfillFile(chunks, filename, { type: mimeType });
272
+ this._formData.set(name, file);
273
+ });
242
274
  });
243
- fileStream.on('close', () => {
244
- if (fileStream.truncated) {
245
- bb.destroy(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
246
- }
247
- const file = new File_js_1.PonyfillFile(chunks, filename, { type: mimeType });
248
- this._formData.set(name, file);
275
+ bb.on('fieldsLimit', () => {
276
+ complete(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
249
277
  });
278
+ bb.on('filesLimit', () => {
279
+ complete(new Error(`Files limit exceeded: ${formDataLimits?.files}`));
280
+ });
281
+ bb.on('partsLimit', () => {
282
+ complete(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
283
+ });
284
+ bb.on('end', complete);
285
+ bb.on('finish', complete);
286
+ bb.on('close', complete);
287
+ bb.on('error', complete);
288
+ stream.pipe(bb);
250
289
  });
251
- bb.on('fieldsLimit', () => {
252
- bb.destroy(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
253
- });
254
- bb.on('filesLimit', () => {
255
- bb.destroy(new Error(`Files limit exceeded: ${formDataLimits?.files}`));
256
- });
257
- bb.on('partsLimit', () => {
258
- bb.destroy(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
259
- });
260
- return (0, promises_1.pipeline)(stream, bb, {
261
- signal: this._signal || undefined,
262
- }).then(() => this._formData);
263
290
  }
264
291
  buffer() {
265
292
  if (this._buffer) {
@@ -296,16 +323,14 @@ class PonyfillBody {
296
323
  });
297
324
  }
298
325
  }
299
- return (0, utils_js_1.fakePromise)()
300
- .then(() => this._collectChunksFromReadable())
301
- .then(chunks => {
326
+ return (0, utils_js_1.fakePromise)((0, promise_helpers_1.handleMaybePromise)(() => this._collectChunksFromReadable(), chunks => {
302
327
  if (chunks.length === 1) {
303
328
  this._buffer = chunks[0];
304
329
  return this._buffer;
305
330
  }
306
331
  this._buffer = node_buffer_1.Buffer.concat(chunks);
307
332
  return this._buffer;
308
- });
333
+ }));
309
334
  }
310
335
  bytes() {
311
336
  return this.buffer();
@@ -348,7 +373,7 @@ class PonyfillBody {
348
373
  }
349
374
  }
350
375
  exports.PonyfillBody = PonyfillBody;
351
- function processBodyInit(bodyInit) {
376
+ function processBodyInit(bodyInit, signal) {
352
377
  if (bodyInit == null) {
353
378
  return {
354
379
  bodyFactory: () => null,
@@ -357,10 +382,11 @@ function processBodyInit(bodyInit) {
357
382
  };
358
383
  }
359
384
  if (typeof bodyInit === 'string') {
385
+ const contentLength = node_buffer_1.Buffer.byteLength(bodyInit);
360
386
  return {
361
387
  bodyType: BodyInitType.String,
362
- contentType: null,
363
- contentLength: null,
388
+ contentType: 'text/plain;charset=UTF-8',
389
+ contentLength,
364
390
  bodyFactory() {
365
391
  const readable = node_stream_1.Readable.from(node_buffer_1.Buffer.from(bodyInit, 'utf-8'));
366
392
  return new ReadableStream_js_1.PonyfillReadableStream(readable);
@@ -430,6 +456,20 @@ function processBodyInit(bodyInit) {
430
456
  },
431
457
  };
432
458
  }
459
+ if (bodyInit instanceof node_http_1.IncomingMessage) {
460
+ const passThrough = (0, utils_js_1.wrapIncomingMessageWithPassthrough)({
461
+ incomingMessage: bodyInit,
462
+ signal,
463
+ });
464
+ return {
465
+ bodyType: BodyInitType.Readable,
466
+ contentType: null,
467
+ contentLength: null,
468
+ bodyFactory() {
469
+ return new ReadableStream_js_1.PonyfillReadableStream(passThrough);
470
+ },
471
+ };
472
+ }
433
473
  if (bodyInit instanceof node_stream_1.Readable) {
434
474
  return {
435
475
  bodyType: BodyInitType.Readable,
package/cjs/Response.js CHANGED
@@ -4,6 +4,7 @@ exports.PonyfillResponse = void 0;
4
4
  const node_http_1 = require("node:http");
5
5
  const Body_js_1 = require("./Body.js");
6
6
  const Headers_js_1 = require("./Headers.js");
7
+ const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
7
8
  class PonyfillResponse extends Body_js_1.PonyfillBody {
8
9
  headers;
9
10
  constructor(body, init) {
@@ -49,6 +50,57 @@ class PonyfillResponse extends Body_js_1.PonyfillBody {
49
50
  }
50
51
  static json(data, init) {
51
52
  const bodyInit = JSON.stringify(data);
53
+ if (!init) {
54
+ init = {
55
+ headers: {
56
+ 'content-type': JSON_CONTENT_TYPE,
57
+ 'content-length': Buffer.byteLength(bodyInit).toString(),
58
+ },
59
+ };
60
+ }
61
+ else if (!init.headers) {
62
+ init.headers = {
63
+ 'content-type': JSON_CONTENT_TYPE,
64
+ 'content-length': Buffer.byteLength(bodyInit).toString(),
65
+ };
66
+ }
67
+ else if ((0, Headers_js_1.isHeadersLike)(init.headers)) {
68
+ if (!init.headers.has('content-type')) {
69
+ init.headers.set('content-type', JSON_CONTENT_TYPE);
70
+ }
71
+ if (!init.headers.has('content-length')) {
72
+ init.headers.set('content-length', Buffer.byteLength(bodyInit).toString());
73
+ }
74
+ }
75
+ else if (Array.isArray(init.headers)) {
76
+ let contentTypeExists = false;
77
+ let contentLengthExists = false;
78
+ for (const [key] of init.headers) {
79
+ if (contentLengthExists && contentTypeExists) {
80
+ break;
81
+ }
82
+ if (!contentTypeExists && key.toLowerCase() === 'content-type') {
83
+ contentTypeExists = true;
84
+ }
85
+ else if (!contentLengthExists && key.toLowerCase() === 'content-length') {
86
+ contentLengthExists = true;
87
+ }
88
+ }
89
+ if (!contentTypeExists) {
90
+ init.headers.push(['content-type', JSON_CONTENT_TYPE]);
91
+ }
92
+ if (!contentLengthExists) {
93
+ init.headers.push(['content-length', Buffer.byteLength(bodyInit).toString()]);
94
+ }
95
+ }
96
+ else if (typeof init.headers === 'object') {
97
+ if (init.headers?.['content-type'] == null) {
98
+ init.headers['content-type'] = JSON_CONTENT_TYPE;
99
+ }
100
+ if (init.headers?.['content-length'] == null) {
101
+ init.headers['content-length'] = Buffer.byteLength(bodyInit).toString();
102
+ }
103
+ }
52
104
  return new PonyfillResponse(bodyInit, init);
53
105
  }
54
106
  [Symbol.toStringTag] = 'Response';
@@ -95,12 +95,14 @@ function fetchNodeHttp(fetchRequest) {
95
95
  return;
96
96
  }
97
97
  }
98
- outputStream = (0, utils_js_1.wrapIncomingMessageWithPassthrough)({
99
- incomingMessage: nodeResponse,
100
- passThrough: outputStream,
101
- signal,
102
- onError: reject,
103
- });
98
+ if (outputStream != null) {
99
+ outputStream = (0, utils_js_1.wrapIncomingMessageWithPassthrough)({
100
+ incomingMessage: nodeResponse,
101
+ passThrough: outputStream,
102
+ signal,
103
+ onError: reject,
104
+ });
105
+ }
104
106
  const statusCode = nodeResponse.statusCode || 200;
105
107
  let statusText = nodeResponse.statusMessage || node_http_1.STATUS_CODES[statusCode];
106
108
  if (statusText == null) {
package/esm/Body.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /* eslint-disable @typescript-eslint/ban-ts-comment */
2
2
  import { Buffer } from 'node:buffer';
3
- import { Readable } from 'node:stream';
4
- import { pipeline } from 'node:stream/promises';
3
+ import { IncomingMessage } from 'node:http';
4
+ import { addAbortSignal, Readable } from 'node:stream';
5
5
  import { Busboy } from '@fastify/busboy';
6
- import { createDeferredPromise, fakeRejectPromise, } from '@whatwg-node/promise-helpers';
6
+ import { handleMaybePromise } from '@whatwg-node/promise-helpers';
7
7
  import { hasArrayBufferMethod, hasBufferMethod, hasBytesMethod, PonyfillBlob } from './Blob.js';
8
8
  import { PonyfillFile } from './File.js';
9
9
  import { getStreamFromFormData, PonyfillFormData } from './FormData.js';
10
10
  import { PonyfillReadableStream } from './ReadableStream.js';
11
- import { fakePromise, isArrayBufferView } from './utils.js';
11
+ import { fakePromise, isArrayBufferView, wrapIncomingMessageWithPassthrough } from './utils.js';
12
12
  var BodyInitType;
13
13
  (function (BodyInitType) {
14
14
  BodyInitType["ReadableStream"] = "ReadableStream";
@@ -30,7 +30,7 @@ export class PonyfillBody {
30
30
  this.bodyInit = bodyInit;
31
31
  this.options = options;
32
32
  this._signal = options.signal || null;
33
- const { bodyFactory, contentType, contentLength, bodyType, buffer } = processBodyInit(bodyInit);
33
+ const { bodyFactory, contentType, contentLength, bodyType, buffer } = processBodyInit(bodyInit, options?.signal);
34
34
  this._bodyFactory = bodyFactory;
35
35
  this.contentType = contentType;
36
36
  this.contentLength = contentLength;
@@ -106,14 +106,14 @@ export class PonyfillBody {
106
106
  _doCollectChunksFromReadableJob() {
107
107
  if (this.bodyType === BodyInitType.AsyncIterable) {
108
108
  if (Array.fromAsync) {
109
- return Array.fromAsync(this.bodyInit).then(chunks => {
109
+ return handleMaybePromise(() => Array.fromAsync(this.bodyInit), chunks => {
110
110
  this._chunks = chunks;
111
111
  return this._chunks;
112
112
  });
113
113
  }
114
114
  const iterator = this.bodyInit[Symbol.asyncIterator]();
115
115
  const chunks = [];
116
- const collectValue = () => iterator.next().then(({ value, done }) => {
116
+ const collectValue = () => handleMaybePromise(() => iterator.next(), ({ value, done }) => {
117
117
  if (value) {
118
118
  chunks.push(value);
119
119
  }
@@ -128,30 +128,23 @@ export class PonyfillBody {
128
128
  const _body = this.generateBody();
129
129
  if (!_body) {
130
130
  this._chunks = [];
131
- return this._chunks;
132
- }
133
- if (_body.readable.destroyed) {
134
- // If the stream is already destroyed, we can resolve immediately
135
- this._chunks = [];
136
- return this._chunks;
131
+ return fakePromise(this._chunks);
137
132
  }
138
133
  const chunks = [];
139
- const deferred = createDeferredPromise();
140
- _body.readable.on('data', function nextChunk(chunk) {
141
- chunks.push(chunk);
142
- });
143
- // eslint-disable-next-line @typescript-eslint/no-this-alias
144
- const _this = this;
145
- _body.readable.once('end', function endOfStream() {
146
- _this._chunks = chunks;
147
- deferred.resolve(chunks);
148
- });
149
- _body.readable.once('error', function errorOnStream(err) {
150
- deferred.reject(err);
134
+ return new Promise((resolve, reject) => {
135
+ _body.readable.on('data', chunk => {
136
+ chunks.push(chunk);
137
+ });
138
+ _body.readable.once('error', reject);
139
+ _body.readable.once('end', () => {
140
+ resolve((this._chunks = chunks));
141
+ });
151
142
  });
152
- return deferred.promise;
153
143
  }
154
144
  _collectChunksFromReadable() {
145
+ if (this._chunks) {
146
+ return fakePromise(this._chunks);
147
+ }
155
148
  this._chunks ||= this._doCollectChunksFromReadableJob();
156
149
  return this._chunks;
157
150
  }
@@ -178,15 +171,13 @@ export class PonyfillBody {
178
171
  });
179
172
  return fakePromise(this._blob);
180
173
  }
181
- return fakePromise()
182
- .then(() => this._collectChunksFromReadable())
183
- .then(chunks => {
174
+ return fakePromise(handleMaybePromise(() => this._collectChunksFromReadable(), chunks => {
184
175
  this._blob = new PonyfillBlob(chunks, {
185
176
  type: this.contentType || '',
186
177
  size: this.contentLength,
187
178
  });
188
179
  return this._blob;
189
- });
180
+ }));
190
181
  }
191
182
  _formData = null;
192
183
  formData(opts) {
@@ -206,57 +197,93 @@ export class PonyfillBody {
206
197
  ...this.options.formDataLimits,
207
198
  ...opts?.formDataLimits,
208
199
  };
209
- const stream = this.body?.readable;
210
- if (!stream) {
211
- return fakeRejectPromise(new Error('No stream available'));
212
- }
213
- const bb = new Busboy({
214
- headers: {
215
- 'content-length': typeof this.contentLength === 'number'
216
- ? this.contentLength.toString()
217
- : this.contentLength || '',
218
- 'content-type': this.contentType || '',
219
- },
220
- limits: formDataLimits,
221
- defCharset: 'utf-8',
222
- });
223
- bb.on('field', (name, value, fieldnameTruncated, valueTruncated) => {
224
- if (fieldnameTruncated) {
225
- bb.destroy(new Error(`Field name size exceeded: ${formDataLimits?.fieldNameSize} bytes`));
200
+ return new Promise((resolve, reject) => {
201
+ const stream = this.body?.readable;
202
+ if (!stream) {
203
+ return reject(new Error('No stream available'));
226
204
  }
227
- if (valueTruncated) {
228
- bb.destroy(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
205
+ // form data file that is currently being processed, it's
206
+ // important to keep track of it in case the stream ends early
207
+ let currFile = null;
208
+ const bb = new Busboy({
209
+ headers: {
210
+ 'content-length': typeof this.contentLength === 'number'
211
+ ? this.contentLength.toString()
212
+ : this.contentLength || '',
213
+ 'content-type': this.contentType || '',
214
+ },
215
+ limits: formDataLimits,
216
+ defCharset: 'utf-8',
217
+ });
218
+ if (this._signal) {
219
+ addAbortSignal(this._signal, bb);
229
220
  }
230
- this._formData.set(name, value);
231
- });
232
- bb.on('file', (name, fileStream, filename, _transferEncoding, mimeType) => {
233
- const chunks = [];
234
- fileStream.on('data', chunk => {
235
- chunks.push(chunk);
221
+ let completed = false;
222
+ const complete = (err) => {
223
+ if (completed)
224
+ return;
225
+ completed = true;
226
+ stream.unpipe(bb);
227
+ bb.destroy();
228
+ if (currFile) {
229
+ currFile.destroy();
230
+ currFile = null;
231
+ }
232
+ if (err) {
233
+ reject(err);
234
+ }
235
+ else {
236
+ // no error occured, this is a successful end/complete/finish
237
+ resolve(this._formData);
238
+ }
239
+ };
240
+ // we dont need to listen to the stream close event because bb will close or error when necessary
241
+ // stream.on('close', complete);
242
+ // stream can be aborted, for example
243
+ stream.on('error', complete);
244
+ bb.on('field', (name, value, fieldnameTruncated, valueTruncated) => {
245
+ if (fieldnameTruncated) {
246
+ return complete(new Error(`Field name size exceeded: ${formDataLimits?.fieldNameSize} bytes`));
247
+ }
248
+ if (valueTruncated) {
249
+ return complete(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
250
+ }
251
+ this._formData.set(name, value);
236
252
  });
237
- fileStream.on('limit', () => {
238
- bb.destroy(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
253
+ bb.on('file', (name, fileStream, filename, _transferEncoding, mimeType) => {
254
+ currFile = fileStream;
255
+ const chunks = [];
256
+ fileStream.on('data', chunk => {
257
+ chunks.push(chunk);
258
+ });
259
+ fileStream.on('error', complete);
260
+ fileStream.on('limit', () => {
261
+ complete(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
262
+ });
263
+ fileStream.on('close', () => {
264
+ if (fileStream.truncated) {
265
+ complete(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
266
+ }
267
+ currFile = null;
268
+ const file = new PonyfillFile(chunks, filename, { type: mimeType });
269
+ this._formData.set(name, file);
270
+ });
239
271
  });
240
- fileStream.on('close', () => {
241
- if (fileStream.truncated) {
242
- bb.destroy(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
243
- }
244
- const file = new PonyfillFile(chunks, filename, { type: mimeType });
245
- this._formData.set(name, file);
272
+ bb.on('fieldsLimit', () => {
273
+ complete(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
246
274
  });
275
+ bb.on('filesLimit', () => {
276
+ complete(new Error(`Files limit exceeded: ${formDataLimits?.files}`));
277
+ });
278
+ bb.on('partsLimit', () => {
279
+ complete(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
280
+ });
281
+ bb.on('end', complete);
282
+ bb.on('finish', complete);
283
+ bb.on('close', complete);
284
+ bb.on('error', complete);
285
+ stream.pipe(bb);
247
286
  });
248
- bb.on('fieldsLimit', () => {
249
- bb.destroy(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
250
- });
251
- bb.on('filesLimit', () => {
252
- bb.destroy(new Error(`Files limit exceeded: ${formDataLimits?.files}`));
253
- });
254
- bb.on('partsLimit', () => {
255
- bb.destroy(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
256
- });
257
- return pipeline(stream, bb, {
258
- signal: this._signal || undefined,
259
- }).then(() => this._formData);
260
287
  }
261
288
  buffer() {
262
289
  if (this._buffer) {
@@ -293,16 +320,14 @@ export class PonyfillBody {
293
320
  });
294
321
  }
295
322
  }
296
- return fakePromise()
297
- .then(() => this._collectChunksFromReadable())
298
- .then(chunks => {
323
+ return fakePromise(handleMaybePromise(() => this._collectChunksFromReadable(), chunks => {
299
324
  if (chunks.length === 1) {
300
325
  this._buffer = chunks[0];
301
326
  return this._buffer;
302
327
  }
303
328
  this._buffer = Buffer.concat(chunks);
304
329
  return this._buffer;
305
- });
330
+ }));
306
331
  }
307
332
  bytes() {
308
333
  return this.buffer();
@@ -344,7 +369,7 @@ export class PonyfillBody {
344
369
  });
345
370
  }
346
371
  }
347
- function processBodyInit(bodyInit) {
372
+ function processBodyInit(bodyInit, signal) {
348
373
  if (bodyInit == null) {
349
374
  return {
350
375
  bodyFactory: () => null,
@@ -353,10 +378,11 @@ function processBodyInit(bodyInit) {
353
378
  };
354
379
  }
355
380
  if (typeof bodyInit === 'string') {
381
+ const contentLength = Buffer.byteLength(bodyInit);
356
382
  return {
357
383
  bodyType: BodyInitType.String,
358
- contentType: null,
359
- contentLength: null,
384
+ contentType: 'text/plain;charset=UTF-8',
385
+ contentLength,
360
386
  bodyFactory() {
361
387
  const readable = Readable.from(Buffer.from(bodyInit, 'utf-8'));
362
388
  return new PonyfillReadableStream(readable);
@@ -426,6 +452,20 @@ function processBodyInit(bodyInit) {
426
452
  },
427
453
  };
428
454
  }
455
+ if (bodyInit instanceof IncomingMessage) {
456
+ const passThrough = wrapIncomingMessageWithPassthrough({
457
+ incomingMessage: bodyInit,
458
+ signal,
459
+ });
460
+ return {
461
+ bodyType: BodyInitType.Readable,
462
+ contentType: null,
463
+ contentLength: null,
464
+ bodyFactory() {
465
+ return new PonyfillReadableStream(passThrough);
466
+ },
467
+ };
468
+ }
429
469
  if (bodyInit instanceof Readable) {
430
470
  return {
431
471
  bodyType: BodyInitType.Readable,
package/esm/Response.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { STATUS_CODES } from 'node:http';
2
2
  import { PonyfillBody } from './Body.js';
3
3
  import { isHeadersLike, PonyfillHeaders } from './Headers.js';
4
+ const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
4
5
  export class PonyfillResponse extends PonyfillBody {
5
6
  headers;
6
7
  constructor(body, init) {
@@ -46,6 +47,57 @@ export class PonyfillResponse extends PonyfillBody {
46
47
  }
47
48
  static json(data, init) {
48
49
  const bodyInit = JSON.stringify(data);
50
+ if (!init) {
51
+ init = {
52
+ headers: {
53
+ 'content-type': JSON_CONTENT_TYPE,
54
+ 'content-length': Buffer.byteLength(bodyInit).toString(),
55
+ },
56
+ };
57
+ }
58
+ else if (!init.headers) {
59
+ init.headers = {
60
+ 'content-type': JSON_CONTENT_TYPE,
61
+ 'content-length': Buffer.byteLength(bodyInit).toString(),
62
+ };
63
+ }
64
+ else if (isHeadersLike(init.headers)) {
65
+ if (!init.headers.has('content-type')) {
66
+ init.headers.set('content-type', JSON_CONTENT_TYPE);
67
+ }
68
+ if (!init.headers.has('content-length')) {
69
+ init.headers.set('content-length', Buffer.byteLength(bodyInit).toString());
70
+ }
71
+ }
72
+ else if (Array.isArray(init.headers)) {
73
+ let contentTypeExists = false;
74
+ let contentLengthExists = false;
75
+ for (const [key] of init.headers) {
76
+ if (contentLengthExists && contentTypeExists) {
77
+ break;
78
+ }
79
+ if (!contentTypeExists && key.toLowerCase() === 'content-type') {
80
+ contentTypeExists = true;
81
+ }
82
+ else if (!contentLengthExists && key.toLowerCase() === 'content-length') {
83
+ contentLengthExists = true;
84
+ }
85
+ }
86
+ if (!contentTypeExists) {
87
+ init.headers.push(['content-type', JSON_CONTENT_TYPE]);
88
+ }
89
+ if (!contentLengthExists) {
90
+ init.headers.push(['content-length', Buffer.byteLength(bodyInit).toString()]);
91
+ }
92
+ }
93
+ else if (typeof init.headers === 'object') {
94
+ if (init.headers?.['content-type'] == null) {
95
+ init.headers['content-type'] = JSON_CONTENT_TYPE;
96
+ }
97
+ if (init.headers?.['content-length'] == null) {
98
+ init.headers['content-length'] = Buffer.byteLength(bodyInit).toString();
99
+ }
100
+ }
49
101
  return new PonyfillResponse(bodyInit, init);
50
102
  }
51
103
  [Symbol.toStringTag] = 'Response';
@@ -92,12 +92,14 @@ export function fetchNodeHttp(fetchRequest) {
92
92
  return;
93
93
  }
94
94
  }
95
- outputStream = wrapIncomingMessageWithPassthrough({
96
- incomingMessage: nodeResponse,
97
- passThrough: outputStream,
98
- signal,
99
- onError: reject,
100
- });
95
+ if (outputStream != null) {
96
+ outputStream = wrapIncomingMessageWithPassthrough({
97
+ incomingMessage: nodeResponse,
98
+ passThrough: outputStream,
99
+ signal,
100
+ onError: reject,
101
+ });
102
+ }
101
103
  const statusCode = nodeResponse.statusCode || 200;
102
104
  let statusText = nodeResponse.statusMessage || STATUS_CODES[statusCode];
103
105
  if (statusText == null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whatwg-node/node-fetch",
3
- "version": "0.7.22-alpha-20250527020114-bf7a9c1c4bf32e1ca6edd3faf2d7a81370ded65c",
3
+ "version": "0.7.22-alpha-20250710160716-6df7209d12299dba404ac539d834654be5d43990",
4
4
  "description": "Fetch API implementation for Node",
5
5
  "sideEffects": false,
6
6
  "dependencies": {
@@ -10,9 +10,9 @@ export declare class PonyfillReadableStream<T> implements ReadableStream<T> {
10
10
  getReader(): ReadableStreamDefaultReader<T>;
11
11
  [Symbol.asyncIterator](): {
12
12
  [Symbol.asyncIterator](): /*elided*/ any;
13
- next: () => Promise<IteratorResult<any, any>>;
14
- return: () => Promise<IteratorResult<any, any>>;
15
- throw: (err: Error) => Promise<IteratorResult<any, any>>;
13
+ next: () => Promise<IteratorResult<any, undefined>>;
14
+ return: () => Promise<IteratorResult<any, undefined>>;
15
+ throw: (err: Error) => Promise<IteratorResult<any, undefined>>;
16
16
  };
17
17
  tee(): [ReadableStream<T>, ReadableStream<T>];
18
18
  private pipeToWriter;
@@ -10,9 +10,9 @@ export declare class PonyfillReadableStream<T> implements ReadableStream<T> {
10
10
  getReader(): ReadableStreamDefaultReader<T>;
11
11
  [Symbol.asyncIterator](): {
12
12
  [Symbol.asyncIterator](): /*elided*/ any;
13
- next: () => Promise<IteratorResult<any, any>>;
14
- return: () => Promise<IteratorResult<any, any>>;
15
- throw: (err: Error) => Promise<IteratorResult<any, any>>;
13
+ next: () => Promise<IteratorResult<any, undefined>>;
14
+ return: () => Promise<IteratorResult<any, undefined>>;
15
+ throw: (err: Error) => Promise<IteratorResult<any, undefined>>;
16
16
  };
17
17
  tee(): [ReadableStream<T>, ReadableStream<T>];
18
18
  private pipeToWriter;