undici 7.15.0 → 7.16.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 (45) hide show
  1. package/README.md +1 -1
  2. package/docs/docs/api/Agent.md +1 -0
  3. package/docs/docs/api/Errors.md +0 -1
  4. package/index-fetch.js +2 -2
  5. package/index.js +4 -8
  6. package/lib/api/api-request.js +22 -8
  7. package/lib/api/readable.js +7 -5
  8. package/lib/core/errors.js +217 -13
  9. package/lib/core/request.js +5 -1
  10. package/lib/core/util.js +32 -10
  11. package/lib/dispatcher/agent.js +19 -7
  12. package/lib/dispatcher/client-h1.js +20 -9
  13. package/lib/dispatcher/client-h2.js +13 -3
  14. package/lib/dispatcher/client.js +57 -57
  15. package/lib/dispatcher/dispatcher-base.js +12 -7
  16. package/lib/dispatcher/env-http-proxy-agent.js +12 -16
  17. package/lib/dispatcher/fixed-queue.js +15 -39
  18. package/lib/dispatcher/h2c-client.js +6 -6
  19. package/lib/dispatcher/pool-base.js +60 -43
  20. package/lib/dispatcher/pool.js +2 -2
  21. package/lib/dispatcher/proxy-agent.js +14 -9
  22. package/lib/global.js +19 -1
  23. package/lib/interceptor/cache.js +61 -0
  24. package/lib/mock/mock-agent.js +4 -4
  25. package/lib/mock/mock-errors.js +10 -0
  26. package/lib/mock/mock-utils.js +12 -10
  27. package/lib/util/date.js +534 -140
  28. package/lib/web/cookies/index.js +1 -1
  29. package/lib/web/eventsource/eventsource-stream.js +2 -2
  30. package/lib/web/eventsource/eventsource.js +34 -29
  31. package/lib/web/eventsource/util.js +1 -9
  32. package/lib/web/fetch/body.js +16 -22
  33. package/lib/web/fetch/index.js +14 -15
  34. package/lib/web/fetch/response.js +2 -4
  35. package/lib/web/fetch/util.js +8 -14
  36. package/lib/web/webidl/index.js +203 -42
  37. package/lib/web/websocket/connection.js +4 -3
  38. package/lib/web/websocket/events.js +1 -1
  39. package/lib/web/websocket/stream/websocketerror.js +22 -1
  40. package/lib/web/websocket/stream/websocketstream.js +16 -7
  41. package/lib/web/websocket/websocket.js +32 -42
  42. package/package.json +7 -6
  43. package/types/agent.d.ts +1 -0
  44. package/types/errors.d.ts +5 -15
  45. package/types/webidl.d.ts +82 -21
@@ -186,7 +186,7 @@ webidl.converters.Cookie = webidl.dictionaryConverter([
186
186
  {
187
187
  converter: webidl.sequenceConverter(webidl.converters.DOMString),
188
188
  key: 'unparsed',
189
- defaultValue: () => new Array(0)
189
+ defaultValue: () => []
190
190
  }
191
191
  ])
192
192
 
@@ -236,7 +236,7 @@ class EventSourceStream extends Transform {
236
236
  this.buffer = this.buffer.subarray(this.pos + 1)
237
237
  this.pos = 0
238
238
  if (
239
- this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
239
+ this.event.data !== undefined || this.event.event || this.event.id !== undefined || this.event.retry) {
240
240
  this.processEvent(this.event)
241
241
  }
242
242
  this.clearEvent()
@@ -367,7 +367,7 @@ class EventSourceStream extends Transform {
367
367
  this.state.reconnectionTime = parseInt(event.retry, 10)
368
368
  }
369
369
 
370
- if (event.id && isValidLastEventId(event.id)) {
370
+ if (event.id !== undefined && isValidLastEventId(event.id)) {
371
371
  this.state.lastEventId = event.id
372
372
  }
373
373
 
@@ -8,7 +8,6 @@ const { EventSourceStream } = require('./eventsource-stream')
8
8
  const { parseMIMEType } = require('../fetch/data-url')
9
9
  const { createFastMessageEvent } = require('../websocket/events')
10
10
  const { isNetworkError } = require('../fetch/response')
11
- const { delay } = require('./util')
12
11
  const { kEnumerableProperty } = require('../../core/util')
13
12
  const { environmentSettingsObject } = require('../fetch/util')
14
13
 
@@ -318,9 +317,9 @@ class EventSource extends EventTarget {
318
317
 
319
318
  /**
320
319
  * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
321
- * @returns {Promise<void>}
320
+ * @returns {void}
322
321
  */
323
- async #reconnect () {
322
+ #reconnect () {
324
323
  // When a user agent is to reestablish the connection, the user agent must
325
324
  // run the following steps. These steps are run in parallel, not as part of
326
325
  // a task. (The tasks that it queues, of course, are run like normal tasks
@@ -338,27 +337,27 @@ class EventSource extends EventTarget {
338
337
  this.dispatchEvent(new Event('error'))
339
338
 
340
339
  // 2. Wait a delay equal to the reconnection time of the event source.
341
- await delay(this.#state.reconnectionTime)
342
-
343
- // 5. Queue a task to run the following steps:
344
-
345
- // 1. If the EventSource object's readyState attribute is not set to
346
- // CONNECTING, then return.
347
- if (this.#readyState !== CONNECTING) return
348
-
349
- // 2. Let request be the EventSource object's request.
350
- // 3. If the EventSource object's last event ID string is not the empty
351
- // string, then:
352
- // 1. Let lastEventIDValue be the EventSource object's last event ID
353
- // string, encoded as UTF-8.
354
- // 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
355
- // list.
356
- if (this.#state.lastEventId.length) {
357
- this.#request.headersList.set('last-event-id', this.#state.lastEventId, true)
358
- }
340
+ setTimeout(() => {
341
+ // 5. Queue a task to run the following steps:
342
+
343
+ // 1. If the EventSource object's readyState attribute is not set to
344
+ // CONNECTING, then return.
345
+ if (this.#readyState !== CONNECTING) return
346
+
347
+ // 2. Let request be the EventSource object's request.
348
+ // 3. If the EventSource object's last event ID string is not the empty
349
+ // string, then:
350
+ // 1. Let lastEventIDValue be the EventSource object's last event ID
351
+ // string, encoded as UTF-8.
352
+ // 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
353
+ // list.
354
+ if (this.#state.lastEventId.length) {
355
+ this.#request.headersList.set('last-event-id', this.#state.lastEventId, true)
356
+ }
359
357
 
360
- // 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
361
- this.#connect()
358
+ // 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
359
+ this.#connect()
360
+ }, this.#state.reconnectionTime)?.unref()
362
361
  }
363
362
 
364
363
  /**
@@ -383,9 +382,11 @@ class EventSource extends EventTarget {
383
382
  this.removeEventListener('open', this.#events.open)
384
383
  }
385
384
 
386
- if (typeof fn === 'function') {
385
+ const listener = webidl.converters.EventHandlerNonNull(fn)
386
+
387
+ if (listener !== null) {
388
+ this.addEventListener('open', listener)
387
389
  this.#events.open = fn
388
- this.addEventListener('open', fn)
389
390
  } else {
390
391
  this.#events.open = null
391
392
  }
@@ -400,9 +401,11 @@ class EventSource extends EventTarget {
400
401
  this.removeEventListener('message', this.#events.message)
401
402
  }
402
403
 
403
- if (typeof fn === 'function') {
404
+ const listener = webidl.converters.EventHandlerNonNull(fn)
405
+
406
+ if (listener !== null) {
407
+ this.addEventListener('message', listener)
404
408
  this.#events.message = fn
405
- this.addEventListener('message', fn)
406
409
  } else {
407
410
  this.#events.message = null
408
411
  }
@@ -417,9 +420,11 @@ class EventSource extends EventTarget {
417
420
  this.removeEventListener('error', this.#events.error)
418
421
  }
419
422
 
420
- if (typeof fn === 'function') {
423
+ const listener = webidl.converters.EventHandlerNonNull(fn)
424
+
425
+ if (listener !== null) {
426
+ this.addEventListener('error', listener)
421
427
  this.#events.error = fn
422
- this.addEventListener('error', fn)
423
428
  } else {
424
429
  this.#events.error = null
425
430
  }
@@ -23,15 +23,7 @@ function isASCIINumber (value) {
23
23
  return true
24
24
  }
25
25
 
26
- // https://github.com/nodejs/undici/issues/2664
27
- function delay (ms) {
28
- return new Promise((resolve) => {
29
- setTimeout(resolve, ms)
30
- })
31
- }
32
-
33
26
  module.exports = {
34
27
  isValidLastEventId,
35
- isASCIINumber,
36
- delay
28
+ isASCIINumber
37
29
  }
@@ -60,7 +60,7 @@ function extractBody (object, keepalive = false) {
60
60
  // 4. Otherwise, set stream to a new ReadableStream object, and set
61
61
  // up stream with byte reading support.
62
62
  stream = new ReadableStream({
63
- async pull (controller) {
63
+ pull (controller) {
64
64
  const buffer = typeof source === 'string' ? textEncoder.encode(source) : source
65
65
 
66
66
  if (buffer.byteLength) {
@@ -110,16 +110,10 @@ function extractBody (object, keepalive = false) {
110
110
 
111
111
  // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
112
112
  type = 'application/x-www-form-urlencoded;charset=UTF-8'
113
- } else if (isArrayBuffer(object)) {
114
- // BufferSource/ArrayBuffer
115
-
116
- // Set source to a copy of the bytes held by object.
117
- source = new Uint8Array(object.slice())
118
- } else if (ArrayBuffer.isView(object)) {
119
- // BufferSource/ArrayBufferView
120
-
121
- // Set source to a copy of the bytes held by object.
122
- source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
113
+ } else if (webidl.is.BufferSource(object)) {
114
+ source = isArrayBuffer(object)
115
+ ? new Uint8Array(object.slice())
116
+ : new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
123
117
  } else if (webidl.is.FormData(object)) {
124
118
  const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
125
119
  const prefix = `--${boundary}\r\nContent-Disposition: form-data`
@@ -320,12 +314,6 @@ function cloneBody (body) {
320
314
  }
321
315
  }
322
316
 
323
- function throwIfAborted (state) {
324
- if (state.aborted) {
325
- throw new DOMException('The operation was aborted.', 'AbortError')
326
- }
327
- }
328
-
329
317
  function bodyMixinMethods (instance, getInternalState) {
330
318
  const methods = {
331
319
  blob () {
@@ -443,24 +431,30 @@ function mixinBody (prototype, getInternalState) {
443
431
  * @param {any} instance
444
432
  * @param {(target: any) => any} getInternalState
445
433
  */
446
- async function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
447
- webidl.brandCheck(object, instance)
434
+ function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
435
+ try {
436
+ webidl.brandCheck(object, instance)
437
+ } catch (e) {
438
+ return Promise.reject(e)
439
+ }
448
440
 
449
441
  const state = getInternalState(object)
450
442
 
451
443
  // 1. If object is unusable, then return a promise rejected
452
444
  // with a TypeError.
453
445
  if (bodyUnusable(state)) {
454
- throw new TypeError('Body is unusable: Body has already been read')
446
+ return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
455
447
  }
456
448
 
457
- throwIfAborted(state)
449
+ if (state.aborted) {
450
+ return Promise.reject(new DOMException('The operation was aborted.', 'AbortError'))
451
+ }
458
452
 
459
453
  // 2. Let promise be a new promise.
460
454
  const promise = createDeferredPromise()
461
455
 
462
456
  // 3. Let errorSteps given error be to reject promise with error.
463
- const errorSteps = (error) => promise.reject(error)
457
+ const errorSteps = promise.reject
464
458
 
465
459
  // 4. Let successSteps given a byte sequence data be to resolve
466
460
  // promise with the result of running convertBytesToJSValue
@@ -63,6 +63,9 @@ const { webidl } = require('../webidl')
63
63
  const { STATUS_CODES } = require('node:http')
64
64
  const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
65
65
  const { createDeferredPromise } = require('../../util/promise')
66
+
67
+ const hasZstd = typeof zlib.createZstdDecompress === 'function'
68
+
66
69
  const GET_OR_HEAD = ['GET', 'HEAD']
67
70
 
68
71
  const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
@@ -2104,33 +2107,29 @@ async function httpNetworkFetch (
2104
2107
  return false
2105
2108
  }
2106
2109
 
2107
- /** @type {string[]} */
2108
- let codings = []
2109
-
2110
2110
  const headersList = new HeadersList()
2111
2111
 
2112
2112
  for (let i = 0; i < rawHeaders.length; i += 2) {
2113
2113
  headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
2114
2114
  }
2115
- const contentEncoding = headersList.get('content-encoding', true)
2116
- if (contentEncoding) {
2117
- // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2118
- // "All content-coding values are case-insensitive..."
2119
- codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2120
- }
2121
2115
  const location = headersList.get('location', true)
2122
2116
 
2123
2117
  this.body = new Readable({ read: resume })
2124
2118
 
2125
- const decoders = []
2126
-
2127
2119
  const willFollow = location && request.redirect === 'follow' &&
2128
2120
  redirectStatusSet.has(status)
2129
2121
 
2122
+ const decoders = []
2123
+
2130
2124
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2131
- if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2125
+ if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2126
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2127
+ const contentEncoding = headersList.get('content-encoding', true)
2128
+ // "All content-coding values are case-insensitive..."
2129
+ /** @type {string[]} */
2130
+ const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
2132
2131
  for (let i = codings.length - 1; i >= 0; --i) {
2133
- const coding = codings[i]
2132
+ const coding = codings[i].trim()
2134
2133
  // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
2135
2134
  if (coding === 'x-gzip' || coding === 'gzip') {
2136
2135
  decoders.push(zlib.createGunzip({
@@ -2151,8 +2150,8 @@ async function httpNetworkFetch (
2151
2150
  flush: zlib.constants.BROTLI_OPERATION_FLUSH,
2152
2151
  finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
2153
2152
  }))
2154
- } else if (coding === 'zstd' && typeof zlib.createZstdDecompress === 'function') {
2155
- // Node.js v23.8.0+ and v22.15.0+ supports Zstandard
2153
+ } else if (coding === 'zstd' && hasZstd) {
2154
+ // Node.js v23.8.0+ and v22.15.0+ supports Zstandard
2156
2155
  decoders.push(zlib.createZstdDecompress({
2157
2156
  flush: zlib.constants.ZSTD_e_continue,
2158
2157
  finishFlush: zlib.constants.ZSTD_e_end
@@ -23,8 +23,6 @@ const { URLSerializer } = require('./data-url')
23
23
  const { kConstruct } = require('../../core/symbols')
24
24
  const assert = require('node:assert')
25
25
 
26
- const { isArrayBuffer } = nodeUtil.types
27
-
28
26
  const textEncoder = new TextEncoder('utf-8')
29
27
 
30
28
  // https://fetch.spec.whatwg.org/#response-class
@@ -120,7 +118,7 @@ class Response {
120
118
  }
121
119
 
122
120
  if (body !== null) {
123
- body = webidl.converters.BodyInit(body)
121
+ body = webidl.converters.BodyInit(body, 'Response', 'body')
124
122
  }
125
123
 
126
124
  init = webidl.converters.ResponseInit(init)
@@ -580,7 +578,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
580
578
  return V
581
579
  }
582
580
 
583
- if (ArrayBuffer.isView(V) || isArrayBuffer(V)) {
581
+ if (webidl.is.BufferSource(V)) {
584
582
  return V
585
583
  }
586
584
 
@@ -502,8 +502,8 @@ function determineRequestsReferrer (request) {
502
502
  if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
503
503
  return 'no-referrer'
504
504
  }
505
- // 2. Return referrerOrigin
506
- return referrerOrigin
505
+ // 2. Return referrerURL.
506
+ return referrerURL
507
507
  }
508
508
  }
509
509
  }
@@ -554,17 +554,11 @@ function stripURLForReferrer (url, originOnly = false) {
554
554
  return url
555
555
  }
556
556
 
557
- const potentialleTrustworthyIPv4RegExp = new RegExp('^(?:' +
558
- '(?:127\\.)' +
559
- '(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){2}' +
560
- '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])' +
561
- ')$')
557
+ const isPotentialleTrustworthyIPv4 = RegExp.prototype.test
558
+ .bind(/^127\.(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/)
562
559
 
563
- const potentialleTrustworthyIPv6RegExp = new RegExp('^(?:' +
564
- '(?:(?:0{1,4}):){7}(?:(?:0{0,3}1))|' +
565
- '(?:(?:0{1,4}):){1,6}(?::(?:0{0,3}1))|' +
566
- '(?:::(?:0{0,3}1))|' +
567
- ')$')
560
+ const isPotentiallyTrustworthyIPv6 = RegExp.prototype.test
561
+ .bind(/^(?:(?:0{1,4}:){7}|(?:0{1,4}:){1,6}:|::)0{0,3}1$/)
568
562
 
569
563
  /**
570
564
  * Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128.
@@ -579,11 +573,11 @@ function isOriginIPPotentiallyTrustworthy (origin) {
579
573
  if (origin[0] === '[' && origin[origin.length - 1] === ']') {
580
574
  origin = origin.slice(1, -1)
581
575
  }
582
- return potentialleTrustworthyIPv6RegExp.test(origin)
576
+ return isPotentiallyTrustworthyIPv6(origin)
583
577
  }
584
578
 
585
579
  // IPv4
586
- return potentialleTrustworthyIPv4RegExp.test(origin)
580
+ return isPotentialleTrustworthyIPv4(origin)
587
581
  }
588
582
 
589
583
  /**