undici 5.5.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -176,7 +176,7 @@ Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
176
176
  * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
177
177
  * https://fetch.spec.whatwg.org/#fetch-method
178
178
 
179
- Only supported on Node 16.5+.
179
+ Only supported on Node 16.8+.
180
180
 
181
181
  This is [experimental](https://nodejs.org/api/documentation.html#documentation_stability_index) and is not yet fully compliant with the Fetch Standard.
182
182
  We plan to ship breaking changes to this feature until it is out of experimental.
@@ -283,6 +283,13 @@ const headers = await fetch(url)
283
283
  .then(res => res.headers)
284
284
  ```
285
285
 
286
+ However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
287
+
288
+ ```js
289
+ const headers = await fetch(url, { method: 'HEAD' })
290
+ .then(res => res.headers)
291
+ ```
292
+
286
293
  ##### Forbidden and Safelisted Header Names
287
294
 
288
295
  * https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
@@ -461,14 +461,14 @@ Arguments:
461
461
  * **options** `RequestOptions`
462
462
  * **callback** `(error: Error | null, data: ResponseData) => void` (optional)
463
463
 
464
- Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed
464
+ Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed.
465
465
 
466
466
  #### Parameter: `RequestOptions`
467
467
 
468
468
  Extends: [`DispatchOptions`](#parameter-dispatchoptions)
469
469
 
470
- * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`
471
- * **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`
470
+ * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`.
471
+ * **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`.
472
472
  * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
473
473
 
474
474
  The `RequestOptions.method` property should not be value `'CONNECT'`.
@@ -476,7 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
476
476
  #### Parameter: `ResponseData`
477
477
 
478
478
  * **statusCode** `number`
479
- * **headers** `http.IncomingHttpHeaders`
479
+ * **headers** `http.IncomingHttpHeaders` - Note that all header keys are lower-cased, e. g. `content-type`.
480
480
  * **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
481
481
  * **trailers** `Record<string, string>` - This object starts out
482
482
  as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
@@ -497,6 +497,8 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
497
497
 
498
498
  - `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144.
499
499
 
500
+ Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.
501
+
500
502
  #### Example 1 - Basic GET Request
501
503
 
502
504
  ```js
@@ -465,7 +465,7 @@ agent.disableNetConnect()
465
465
  agent
466
466
  .get('https://example.com')
467
467
  .intercept({ method: 'GET', path: '/' })
468
- .reply(200, '')
468
+ .reply(200)
469
469
 
470
470
  const pendingInterceptors = agent.pendingInterceptors()
471
471
  // Returns [
@@ -508,7 +508,7 @@ agent.disableNetConnect()
508
508
  agent
509
509
  .get('https://example.com')
510
510
  .intercept({ method: 'GET', path: '/' })
511
- .reply(200, '')
511
+ .reply(200)
512
512
 
513
513
  agent.assertNoPendingInterceptors()
514
514
  // Throws an UndiciError with the following message:
package/index.js CHANGED
@@ -80,7 +80,7 @@ function makeDispatcher (fn) {
80
80
  module.exports.setGlobalDispatcher = setGlobalDispatcher
81
81
  module.exports.getGlobalDispatcher = getGlobalDispatcher
82
82
 
83
- if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 5)) {
83
+ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
84
84
  let fetchImpl = null
85
85
  module.exports.fetch = async function fetch (resource) {
86
86
  if (!fetchImpl) {
@@ -15,7 +15,7 @@ class ConnectHandler extends AsyncResource {
15
15
  throw new InvalidArgumentError('invalid callback')
16
16
  }
17
17
 
18
- const { signal, opaque, responseHeaders, httpTunnel } = opts
18
+ const { signal, opaque, responseHeaders } = opts
19
19
 
20
20
  if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
21
21
  throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
@@ -27,7 +27,6 @@ class ConnectHandler extends AsyncResource {
27
27
  this.responseHeaders = responseHeaders || null
28
28
  this.callback = callback
29
29
  this.abort = null
30
- this.httpTunnel = httpTunnel
31
30
 
32
31
  addSignal(this, signal)
33
32
  }
@@ -41,23 +40,8 @@ class ConnectHandler extends AsyncResource {
41
40
  this.context = context
42
41
  }
43
42
 
44
- onHeaders (statusCode) {
45
- // when httpTunnel headers are allowed
46
- if (this.httpTunnel) {
47
- const { callback, opaque } = this
48
- if (statusCode !== 200) {
49
- if (callback) {
50
- this.callback = null
51
- const err = new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')
52
- queueMicrotask(() => {
53
- this.runInAsyncScope(callback, null, err, { opaque })
54
- })
55
- }
56
- return 1
57
- }
58
- } else {
59
- throw new SocketError('bad connect', null)
60
- }
43
+ onHeaders () {
44
+ throw new SocketError('bad connect', null)
61
45
  }
62
46
 
63
47
  onUpgrade (statusCode, rawHeaders, socket) {
@@ -84,7 +84,8 @@ class RequestHandler extends AsyncResource {
84
84
  }
85
85
 
86
86
  const parsedHeaders = util.parseHeaders(rawHeaders)
87
- const body = new Readable(resume, abort, parsedHeaders['content-type'])
87
+ const contentType = parsedHeaders['content-type']
88
+ const body = new Readable(resume, abort, contentType)
88
89
 
89
90
  this.callback = null
90
91
  this.res = body
@@ -92,8 +93,8 @@ class RequestHandler extends AsyncResource {
92
93
 
93
94
  if (callback !== null) {
94
95
  if (this.throwOnError && statusCode >= 400) {
95
- this.runInAsyncScope(callback, null,
96
- new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)
96
+ this.runInAsyncScope(getResolveErrorBodyCallback, null,
97
+ { callback, body, contentType, statusCode, statusMessage, headers }
97
98
  )
98
99
  return
99
100
  }
@@ -152,6 +153,33 @@ class RequestHandler extends AsyncResource {
152
153
  }
153
154
  }
154
155
 
156
+ async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
157
+ if (statusCode === 204 || !contentType) {
158
+ body.dump()
159
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
160
+ return
161
+ }
162
+
163
+ try {
164
+ if (contentType.startsWith('application/json')) {
165
+ const payload = await body.json()
166
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
167
+ return
168
+ }
169
+
170
+ if (contentType.startsWith('text/')) {
171
+ const payload = await body.text()
172
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
173
+ return
174
+ }
175
+ } catch (err) {
176
+ // Process in a fallback if error
177
+ }
178
+
179
+ body.dump()
180
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
181
+ }
182
+
155
183
  function request (opts, callback) {
156
184
  if (callback === undefined) {
157
185
  return new Promise((resolve, reject) => {
package/lib/client.js CHANGED
@@ -720,7 +720,7 @@ class Parser {
720
720
  }
721
721
  }
722
722
 
723
- if (request.method === 'CONNECT' && statusCode >= 200 && statusCode < 300) {
723
+ if (request.method === 'CONNECT') {
724
724
  assert(client[kRunning] === 1)
725
725
  this.upgrade = true
726
726
  return 2
@@ -889,10 +889,8 @@ function onParserTimeout (parser) {
889
889
 
890
890
  /* istanbul ignore else */
891
891
  if (timeoutType === TIMEOUT_HEADERS) {
892
- if (!socket[kWriting]) {
893
- assert(!parser.paused, 'cannot be paused while waiting for headers')
894
- util.destroy(socket, new HeadersTimeoutError())
895
- }
892
+ assert(!parser.paused, 'cannot be paused while waiting for headers')
893
+ util.destroy(socket, new HeadersTimeoutError())
896
894
  } else if (timeoutType === TIMEOUT_BODY) {
897
895
  if (!parser.paused) {
898
896
  util.destroy(socket, new BodyTimeoutError())
@@ -1,13 +1,5 @@
1
1
  'use strict'
2
2
 
3
- class AbortError extends Error {
4
- constructor () {
5
- super('The operation was aborted')
6
- this.code = 'ABORT_ERR'
7
- this.name = 'AbortError'
8
- }
9
- }
10
-
11
3
  class UndiciError extends Error {
12
4
  constructor (message) {
13
5
  super(message)
@@ -57,12 +49,13 @@ class BodyTimeoutError extends UndiciError {
57
49
  }
58
50
 
59
51
  class ResponseStatusCodeError extends UndiciError {
60
- constructor (message, statusCode, headers) {
52
+ constructor (message, statusCode, headers, body) {
61
53
  super(message)
62
54
  Error.captureStackTrace(this, ResponseStatusCodeError)
63
55
  this.name = 'ResponseStatusCodeError'
64
56
  this.message = message || 'Response Status Code Error'
65
57
  this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
58
+ this.body = body
66
59
  this.status = statusCode
67
60
  this.statusCode = statusCode
68
61
  this.headers = headers
@@ -191,7 +184,6 @@ class HTTPParserError extends Error {
191
184
  }
192
185
 
193
186
  module.exports = {
194
- AbortError,
195
187
  HTTPParserError,
196
188
  UndiciError,
197
189
  HeadersTimeoutError,
@@ -140,8 +140,8 @@ class Request {
140
140
  }
141
141
 
142
142
  if (util.isFormDataLike(this.body)) {
143
- if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) {
144
- throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.')
143
+ if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) {
144
+ throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
145
145
  }
146
146
 
147
147
  if (!extractBody) {
package/lib/fetch/body.js CHANGED
@@ -4,6 +4,7 @@ const util = require('../core/util')
4
4
  const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util')
5
5
  const { FormData } = require('./formdata')
6
6
  const { kState } = require('./symbols')
7
+ const { webidl } = require('./webidl')
7
8
  const { Blob } = require('buffer')
8
9
  const { kBodyUsed } = require('../core/symbols')
9
10
  const assert = require('assert')
@@ -14,12 +15,7 @@ const { isUint8Array, isArrayBuffer } = require('util/types')
14
15
  let ReadableStream
15
16
 
16
17
  async function * blobGen (blob) {
17
- if (blob.stream) {
18
- yield * blob.stream()
19
- } else {
20
- // istanbul ignore next: node < 16.7
21
- yield await blob.arrayBuffer()
22
- }
18
+ yield * blob.stream()
23
19
  }
24
20
 
25
21
  // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
@@ -262,100 +258,188 @@ function cloneBody (body) {
262
258
  }
263
259
  }
264
260
 
265
- const methods = {
266
- async blob () {
267
- const chunks = []
261
+ async function * consumeBody (body) {
262
+ if (body) {
263
+ if (isUint8Array(body)) {
264
+ yield body
265
+ } else {
266
+ const stream = body.stream
268
267
 
269
- if (this[kState].body) {
270
- if (isUint8Array(this[kState].body)) {
271
- chunks.push(this[kState].body)
272
- } else {
273
- const stream = this[kState].body.stream
268
+ if (util.isDisturbed(stream)) {
269
+ throw new TypeError('disturbed')
270
+ }
274
271
 
275
- if (util.isDisturbed(stream)) {
276
- throw new TypeError('disturbed')
277
- }
272
+ if (stream.locked) {
273
+ throw new TypeError('locked')
274
+ }
278
275
 
279
- if (stream.locked) {
280
- throw new TypeError('locked')
281
- }
276
+ // Compat.
277
+ stream[kBodyUsed] = true
278
+
279
+ yield * stream
280
+ }
281
+ }
282
+ }
283
+
284
+ function bodyMixinMethods (instance) {
285
+ const methods = {
286
+ async blob () {
287
+ if (!(this instanceof instance)) {
288
+ throw new TypeError('Illegal invocation')
289
+ }
282
290
 
283
- // Compat.
284
- stream[kBodyUsed] = true
291
+ const chunks = []
285
292
 
286
- for await (const chunk of stream) {
287
- chunks.push(chunk)
293
+ for await (const chunk of consumeBody(this[kState].body)) {
294
+ // Assemble one final large blob with Uint8Array's can exhaust memory.
295
+ // That's why we create create multiple blob's and using references
296
+ chunks.push(new Blob([chunk]))
297
+ }
298
+
299
+ return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
300
+ },
301
+
302
+ async arrayBuffer () {
303
+ if (!(this instanceof instance)) {
304
+ throw new TypeError('Illegal invocation')
305
+ }
306
+
307
+ const contentLength = this.headers.get('content-length')
308
+ const encoded = this.headers.has('content-encoding')
309
+
310
+ // if we have content length and no encoding, then we can
311
+ // pre allocate the buffer and just read the data into it
312
+ if (!encoded && contentLength) {
313
+ const buffer = new Uint8Array(contentLength)
314
+ let offset = 0
315
+
316
+ for await (const chunk of consumeBody(this[kState].body)) {
317
+ buffer.set(chunk, offset)
318
+ offset += chunk.length
288
319
  }
320
+
321
+ return buffer.buffer
289
322
  }
290
- }
291
323
 
292
- return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
293
- },
324
+ // if we don't have content length, then we have to allocate 2x the
325
+ // size of the body, once for consumed data, and once for the final buffer
294
326
 
295
- async arrayBuffer () {
296
- const blob = await this.blob()
297
- return await blob.arrayBuffer()
298
- },
327
+ // This could be optimized by using growable ArrayBuffer, but it's not
328
+ // implemented yet. https://github.com/tc39/proposal-resizablearraybuffer
299
329
 
300
- async text () {
301
- const blob = await this.blob()
302
- return toUSVString(await blob.text())
303
- },
330
+ const chunks = []
331
+ let size = 0
304
332
 
305
- async json () {
306
- return JSON.parse(await this.text())
307
- },
333
+ for await (const chunk of consumeBody(this[kState].body)) {
334
+ chunks.push(chunk)
335
+ size += chunk.byteLength
336
+ }
337
+
338
+ const buffer = new Uint8Array(size)
339
+ let offset = 0
308
340
 
309
- async formData () {
310
- const contentType = this.headers.get('Content-Type')
311
-
312
- // If mimeType’s essence is "multipart/form-data", then:
313
- if (/multipart\/form-data/.test(contentType)) {
314
- throw new NotSupportedError('multipart/form-data not supported')
315
- } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
316
- // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
317
-
318
- // 1. Let entries be the result of parsing bytes.
319
- let entries
320
- try {
321
- entries = new URLSearchParams(await this.text())
322
- } catch (err) {
323
- // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
324
- // 2. If entries is failure, then throw a TypeError.
325
- throw Object.assign(new TypeError(), { cause: err })
341
+ for (const chunk of chunks) {
342
+ buffer.set(chunk, offset)
343
+ offset += chunk.byteLength
326
344
  }
327
345
 
328
- // 3. Return a new FormData object whose entries are entries.
329
- const formData = new FormData()
330
- for (const [name, value] of entries) {
331
- formData.append(name, value)
346
+ return buffer.buffer
347
+ },
348
+
349
+ async text () {
350
+ if (!(this instanceof instance)) {
351
+ throw new TypeError('Illegal invocation')
352
+ }
353
+
354
+ let result = ''
355
+ const textDecoder = new TextDecoder()
356
+
357
+ for await (const chunk of consumeBody(this[kState].body)) {
358
+ result += textDecoder.decode(chunk, { stream: true })
359
+ }
360
+
361
+ // flush
362
+ result += textDecoder.decode()
363
+
364
+ return result
365
+ },
366
+
367
+ async json () {
368
+ if (!(this instanceof instance)) {
369
+ throw new TypeError('Illegal invocation')
370
+ }
371
+
372
+ return JSON.parse(await this.text())
373
+ },
374
+
375
+ async formData () {
376
+ if (!(this instanceof instance)) {
377
+ throw new TypeError('Illegal invocation')
378
+ }
379
+
380
+ const contentType = this.headers.get('Content-Type')
381
+
382
+ // If mimeType’s essence is "multipart/form-data", then:
383
+ if (/multipart\/form-data/.test(contentType)) {
384
+ throw new NotSupportedError('multipart/form-data not supported')
385
+ } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
386
+ // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
387
+
388
+ // 1. Let entries be the result of parsing bytes.
389
+ let entries
390
+ try {
391
+ entries = new URLSearchParams(await this.text())
392
+ } catch (err) {
393
+ // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
394
+ // 2. If entries is failure, then throw a TypeError.
395
+ throw Object.assign(new TypeError(), { cause: err })
396
+ }
397
+
398
+ // 3. Return a new FormData object whose entries are entries.
399
+ const formData = new FormData()
400
+ for (const [name, value] of entries) {
401
+ formData.append(name, value)
402
+ }
403
+ return formData
404
+ } else {
405
+ // Otherwise, throw a TypeError.
406
+ webidl.errors.exception({
407
+ header: `${instance.name}.formData`,
408
+ value: 'Could not parse content as FormData.'
409
+ })
332
410
  }
333
- return formData
334
- } else {
335
- // Otherwise, throw a TypeError.
336
- throw new TypeError()
337
411
  }
338
412
  }
413
+
414
+ return methods
339
415
  }
340
416
 
341
417
  const properties = {
342
418
  body: {
343
419
  enumerable: true,
344
420
  get () {
421
+ if (!this || !this[kState]) {
422
+ throw new TypeError('Illegal invocation')
423
+ }
424
+
345
425
  return this[kState].body ? this[kState].body.stream : null
346
426
  }
347
427
  },
348
428
  bodyUsed: {
349
429
  enumerable: true,
350
430
  get () {
431
+ if (!this || !this[kState]) {
432
+ throw new TypeError('Illegal invocation')
433
+ }
434
+
351
435
  return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
352
436
  }
353
437
  }
354
438
  }
355
439
 
356
440
  function mixinBody (prototype) {
357
- Object.assign(prototype, methods)
358
- Object.defineProperties(prototype, properties)
441
+ Object.assign(prototype.prototype, bodyMixinMethods(prototype))
442
+ Object.defineProperties(prototype.prototype, properties)
359
443
  }
360
444
 
361
445
  module.exports = {
@@ -60,7 +60,19 @@ const subresource = [
60
60
  ''
61
61
  ]
62
62
 
63
+ /** @type {globalThis['DOMException']} */
64
+ const DOMException = globalThis.DOMException ?? (() => {
65
+ // DOMException was only made a global in Node v17.0.0,
66
+ // but fetch supports >= v16.8.
67
+ try {
68
+ atob('~')
69
+ } catch (err) {
70
+ return Object.getPrototypeOf(err).constructor
71
+ }
72
+ })()
73
+
63
74
  module.exports = {
75
+ DOMException,
64
76
  subresource,
65
77
  forbiddenMethods,
66
78
  requestBodyHeader,