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
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { InvalidArgumentError } = require('../core/errors')
3
+ const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
4
4
  const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
5
5
  const DispatcherBase = require('./dispatcher-base')
6
6
  const Pool = require('./pool')
@@ -13,6 +13,7 @@ const kOnConnectionError = Symbol('onConnectionError')
13
13
  const kOnDrain = Symbol('onDrain')
14
14
  const kFactory = Symbol('factory')
15
15
  const kOptions = Symbol('options')
16
+ const kOrigins = Symbol('origins')
16
17
 
17
18
  function defaultFactory (origin, opts) {
18
19
  return opts && opts.connections === 1
@@ -21,7 +22,7 @@ function defaultFactory (origin, opts) {
21
22
  }
22
23
 
23
24
  class Agent extends DispatcherBase {
24
- constructor ({ factory = defaultFactory, connect, ...options } = {}) {
25
+ constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
25
26
  if (typeof factory !== 'function') {
26
27
  throw new InvalidArgumentError('factory must be a function.')
27
28
  }
@@ -30,15 +31,20 @@ class Agent extends DispatcherBase {
30
31
  throw new InvalidArgumentError('connect must be a function or an object')
31
32
  }
32
33
 
34
+ if (typeof maxOrigins !== 'number' || Number.isNaN(maxOrigins) || maxOrigins <= 0) {
35
+ throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
36
+ }
37
+
33
38
  super()
34
39
 
35
40
  if (connect && typeof connect !== 'function') {
36
41
  connect = { ...connect }
37
42
  }
38
43
 
39
- this[kOptions] = { ...util.deepClone(options), connect }
44
+ this[kOptions] = { ...util.deepClone(options), maxOrigins, connect }
40
45
  this[kFactory] = factory
41
46
  this[kClients] = new Map()
47
+ this[kOrigins] = new Set()
42
48
 
43
49
  this[kOnDrain] = (origin, targets) => {
44
50
  this.emit('drain', origin, [this, ...targets])
@@ -73,6 +79,10 @@ class Agent extends DispatcherBase {
73
79
  throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
74
80
  }
75
81
 
82
+ if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) {
83
+ throw new MaxOriginsReachedError()
84
+ }
85
+
76
86
  const result = this[kClients].get(key)
77
87
  let dispatcher = result && result.dispatcher
78
88
  if (!dispatcher) {
@@ -84,6 +94,7 @@ class Agent extends DispatcherBase {
84
94
  this[kClients].delete(key)
85
95
  result.dispatcher.close()
86
96
  }
97
+ this[kOrigins].delete(key)
87
98
  }
88
99
  }
89
100
  dispatcher = this[kFactory](opts.origin, this[kOptions])
@@ -105,29 +116,30 @@ class Agent extends DispatcherBase {
105
116
  })
106
117
 
107
118
  this[kClients].set(key, { count: 0, dispatcher })
119
+ this[kOrigins].add(key)
108
120
  }
109
121
 
110
122
  return dispatcher.dispatch(opts, handler)
111
123
  }
112
124
 
113
- async [kClose] () {
125
+ [kClose] () {
114
126
  const closePromises = []
115
127
  for (const { dispatcher } of this[kClients].values()) {
116
128
  closePromises.push(dispatcher.close())
117
129
  }
118
130
  this[kClients].clear()
119
131
 
120
- await Promise.all(closePromises)
132
+ return Promise.all(closePromises)
121
133
  }
122
134
 
123
- async [kDestroy] (err) {
135
+ [kDestroy] (err) {
124
136
  const destroyPromises = []
125
137
  for (const { dispatcher } of this[kClients].values()) {
126
138
  destroyPromises.push(dispatcher.destroy(err))
127
139
  }
128
140
  this[kClients].clear()
129
141
 
130
- await Promise.all(destroyPromises)
142
+ return Promise.all(destroyPromises)
131
143
  }
132
144
 
133
145
  get stats () {
@@ -64,11 +64,26 @@ function lazyllhttp () {
64
64
  const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
65
65
 
66
66
  let mod
67
- try {
68
- mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
69
- } catch {
70
- /* istanbul ignore next */
71
67
 
68
+ // We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
69
+ let useWasmSIMD = process.arch !== 'ppc64'
70
+ // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
71
+ if (process.env.UNDICI_NO_WASM_SIMD === '1') {
72
+ useWasmSIMD = true
73
+ } else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
74
+ useWasmSIMD = false
75
+ }
76
+
77
+ if (useWasmSIMD) {
78
+ try {
79
+ mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
80
+ /* istanbul ignore next */
81
+ } catch {
82
+ }
83
+ }
84
+
85
+ /* istanbul ignore next */
86
+ if (!mod) {
72
87
  // We could check if the error was caused by the simd option not
73
88
  // being enabled, but the occurring of this other error
74
89
  // * https://github.com/emscripten-core/emscripten/issues/11495
@@ -325,10 +340,6 @@ class Parser {
325
340
  currentBufferRef = chunk
326
341
  currentParser = this
327
342
  ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, chunk.length)
328
- /* eslint-disable-next-line no-useless-catch */
329
- } catch (err) {
330
- /* istanbul ignore next: difficult to make a test case for */
331
- throw err
332
343
  } finally {
333
344
  currentParser = null
334
345
  currentBufferRef = null
@@ -760,7 +771,7 @@ function onParserTimeout (parser) {
760
771
  * @param {import('net').Socket} socket
761
772
  * @returns
762
773
  */
763
- async function connectH1 (client, socket) {
774
+ function connectH1 (client, socket) {
764
775
  client[kSocket] = socket
765
776
 
766
777
  if (!llhttpInstance) {
@@ -77,7 +77,7 @@ function parseH2Headers (headers) {
77
77
  return result
78
78
  }
79
79
 
80
- async function connectH2 (client, socket) {
80
+ function connectH2 (client, socket) {
81
81
  client[kSocket] = socket
82
82
 
83
83
  const session = http2.connect(client[kUrl], {
@@ -279,7 +279,7 @@ function shouldSendContentLength (method) {
279
279
  function writeH2 (client, request) {
280
280
  const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
281
281
  const session = client[kHTTP2Session]
282
- const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
282
+ const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
283
283
  let { body } = request
284
284
 
285
285
  if (upgrade) {
@@ -292,6 +292,16 @@ function writeH2 (client, request) {
292
292
  const key = reqHeaders[n + 0]
293
293
  const val = reqHeaders[n + 1]
294
294
 
295
+ if (key === 'cookie') {
296
+ if (headers[key] != null) {
297
+ headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
298
+ } else {
299
+ headers[key] = val
300
+ }
301
+
302
+ continue
303
+ }
304
+
295
305
  if (Array.isArray(val)) {
296
306
  for (let i = 0; i < val.length; i++) {
297
307
  if (headers[key]) {
@@ -387,7 +397,7 @@ function writeH2 (client, request) {
387
397
  // :path and :scheme headers must be omitted when sending CONNECT
388
398
 
389
399
  headers[HTTP2_HEADER_PATH] = path
390
- headers[HTTP2_HEADER_SCHEME] = 'https'
400
+ headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
391
401
 
392
402
  // https://tools.ietf.org/html/rfc7231#section-4.3.1
393
403
  // https://tools.ietf.org/html/rfc7231#section-4.3.2
@@ -296,8 +296,7 @@ class Client extends DispatcherBase {
296
296
  }
297
297
 
298
298
  [kDispatch] (opts, handler) {
299
- const origin = opts.origin || this[kUrl].origin
300
- const request = new Request(origin, opts, handler)
299
+ const request = new Request(this[kUrl].origin, opts, handler)
301
300
 
302
301
  this[kQueue].push(request)
303
302
  if (this[kResuming]) {
@@ -317,7 +316,7 @@ class Client extends DispatcherBase {
317
316
  return this[kNeedDrain] < 2
318
317
  }
319
318
 
320
- async [kClose] () {
319
+ [kClose] () {
321
320
  // TODO: for H2 we need to gracefully flush the remaining enqueued
322
321
  // request and close each stream.
323
322
  return new Promise((resolve) => {
@@ -329,7 +328,7 @@ class Client extends DispatcherBase {
329
328
  })
330
329
  }
331
330
 
332
- async [kDestroy] (err) {
331
+ [kDestroy] (err) {
333
332
  return new Promise((resolve) => {
334
333
  const requests = this[kQueue].splice(this[kPendingIdx])
335
334
  for (let i = 0; i < requests.length; i++) {
@@ -381,9 +380,9 @@ function onError (client, err) {
381
380
 
382
381
  /**
383
382
  * @param {Client} client
384
- * @returns
383
+ * @returns {void}
385
384
  */
386
- async function connect (client) {
385
+ function connect (client) {
387
386
  assert(!client[kConnecting])
388
387
  assert(!client[kHTTPContext])
389
388
 
@@ -417,26 +416,23 @@ async function connect (client) {
417
416
  })
418
417
  }
419
418
 
420
- try {
421
- const socket = await new Promise((resolve, reject) => {
422
- client[kConnector]({
423
- host,
424
- hostname,
425
- protocol,
426
- port,
427
- servername: client[kServerName],
428
- localAddress: client[kLocalAddress]
429
- }, (err, socket) => {
430
- if (err) {
431
- reject(err)
432
- } else {
433
- resolve(socket)
434
- }
435
- })
436
- })
419
+ client[kConnector]({
420
+ host,
421
+ hostname,
422
+ protocol,
423
+ port,
424
+ servername: client[kServerName],
425
+ localAddress: client[kLocalAddress]
426
+ }, (err, socket) => {
427
+ if (err) {
428
+ handleConnectError(client, err, { host, hostname, protocol, port })
429
+ client[kResume]()
430
+ return
431
+ }
437
432
 
438
433
  if (client.destroyed) {
439
434
  util.destroy(socket.on('error', noop), new ClientDestroyedError())
435
+ client[kResume]()
440
436
  return
441
437
  }
442
438
 
@@ -444,11 +440,13 @@ async function connect (client) {
444
440
 
445
441
  try {
446
442
  client[kHTTPContext] = socket.alpnProtocol === 'h2'
447
- ? await connectH2(client, socket)
448
- : await connectH1(client, socket)
443
+ ? connectH2(client, socket)
444
+ : connectH1(client, socket)
449
445
  } catch (err) {
450
446
  socket.destroy().on('error', noop)
451
- throw err
447
+ handleConnectError(client, err, { host, hostname, protocol, port })
448
+ client[kResume]()
449
+ return
452
450
  }
453
451
 
454
452
  client[kConnecting] = false
@@ -473,44 +471,46 @@ async function connect (client) {
473
471
  socket
474
472
  })
475
473
  }
474
+
476
475
  client.emit('connect', client[kUrl], [client])
477
- } catch (err) {
478
- if (client.destroyed) {
479
- return
480
- }
476
+ client[kResume]()
477
+ })
478
+ }
481
479
 
482
- client[kConnecting] = false
480
+ function handleConnectError (client, err, { host, hostname, protocol, port }) {
481
+ if (client.destroyed) {
482
+ return
483
+ }
483
484
 
484
- if (channels.connectError.hasSubscribers) {
485
- channels.connectError.publish({
486
- connectParams: {
487
- host,
488
- hostname,
489
- protocol,
490
- port,
491
- version: client[kHTTPContext]?.version,
492
- servername: client[kServerName],
493
- localAddress: client[kLocalAddress]
494
- },
495
- connector: client[kConnector],
496
- error: err
497
- })
498
- }
485
+ client[kConnecting] = false
499
486
 
500
- if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
501
- assert(client[kRunning] === 0)
502
- while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
503
- const request = client[kQueue][client[kPendingIdx]++]
504
- util.errorRequest(client, request, err)
505
- }
506
- } else {
507
- onError(client, err)
508
- }
487
+ if (channels.connectError.hasSubscribers) {
488
+ channels.connectError.publish({
489
+ connectParams: {
490
+ host,
491
+ hostname,
492
+ protocol,
493
+ port,
494
+ version: client[kHTTPContext]?.version,
495
+ servername: client[kServerName],
496
+ localAddress: client[kLocalAddress]
497
+ },
498
+ connector: client[kConnector],
499
+ error: err
500
+ })
501
+ }
509
502
 
510
- client.emit('connectionError', client[kUrl], [client], err)
503
+ if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
504
+ assert(client[kRunning] === 0)
505
+ while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
506
+ const request = client[kQueue][client[kPendingIdx]++]
507
+ util.errorRequest(client, request, err)
508
+ }
509
+ } else {
510
+ onError(client, err)
511
511
  }
512
512
 
513
- client[kResume]()
513
+ client.emit('connectionError', client[kUrl], [client], err)
514
514
  }
515
515
 
516
516
  function emitDrain (client) {
@@ -13,19 +13,24 @@ const kOnDestroyed = Symbol('onDestroyed')
13
13
  const kOnClosed = Symbol('onClosed')
14
14
 
15
15
  class DispatcherBase extends Dispatcher {
16
- constructor () {
17
- super()
16
+ /** @type {boolean} */
17
+ [kDestroyed] = false;
18
18
 
19
- this[kDestroyed] = false
20
- this[kOnDestroyed] = null
21
- this[kClosed] = false
22
- this[kOnClosed] = []
23
- }
19
+ /** @type {Array|null} */
20
+ [kOnDestroyed] = null;
21
+
22
+ /** @type {boolean} */
23
+ [kClosed] = false;
24
+
25
+ /** @type {Array} */
26
+ [kOnClosed] = []
24
27
 
28
+ /** @returns {boolean} */
25
29
  get destroyed () {
26
30
  return this[kDestroyed]
27
31
  }
28
32
 
33
+ /** @returns {boolean} */
29
34
  get closed () {
30
35
  return this[kClosed]
31
36
  }
@@ -46,24 +46,20 @@ class EnvHttpProxyAgent extends DispatcherBase {
46
46
  return agent.dispatch(opts, handler)
47
47
  }
48
48
 
49
- async [kClose] () {
50
- await this[kNoProxyAgent].close()
51
- if (!this[kHttpProxyAgent][kClosed]) {
52
- await this[kHttpProxyAgent].close()
53
- }
54
- if (!this[kHttpsProxyAgent][kClosed]) {
55
- await this[kHttpsProxyAgent].close()
56
- }
49
+ [kClose] () {
50
+ return Promise.all([
51
+ this[kNoProxyAgent].close(),
52
+ !this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(),
53
+ !this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close()
54
+ ])
57
55
  }
58
56
 
59
- async [kDestroy] (err) {
60
- await this[kNoProxyAgent].destroy(err)
61
- if (!this[kHttpProxyAgent][kDestroyed]) {
62
- await this[kHttpProxyAgent].destroy(err)
63
- }
64
- if (!this[kHttpsProxyAgent][kDestroyed]) {
65
- await this[kHttpsProxyAgent].destroy(err)
66
- }
57
+ [kDestroy] (err) {
58
+ return Promise.all([
59
+ this[kNoProxyAgent].destroy(err),
60
+ !this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err),
61
+ !this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err)
62
+ ])
67
63
  }
68
64
 
69
65
  #getProxyAgentForUrl (url) {
@@ -59,35 +59,21 @@ const kMask = kSize - 1
59
59
  * @template T
60
60
  */
61
61
  class FixedCircularBuffer {
62
- constructor () {
63
- /**
64
- * @type {number}
65
- */
66
- this.bottom = 0
67
- /**
68
- * @type {number}
69
- */
70
- this.top = 0
71
- /**
72
- * @type {Array<T|undefined>}
73
- */
74
- this.list = new Array(kSize).fill(undefined)
75
- /**
76
- * @type {T|null}
77
- */
78
- this.next = null
79
- }
62
+ /** @type {number} */
63
+ bottom = 0
64
+ /** @type {number} */
65
+ top = 0
66
+ /** @type {Array<T|undefined>} */
67
+ list = new Array(kSize).fill(undefined)
68
+ /** @type {T|null} */
69
+ next = null
80
70
 
81
- /**
82
- * @returns {boolean}
83
- */
71
+ /** @returns {boolean} */
84
72
  isEmpty () {
85
73
  return this.top === this.bottom
86
74
  }
87
75
 
88
- /**
89
- * @returns {boolean}
90
- */
76
+ /** @returns {boolean} */
91
77
  isFull () {
92
78
  return ((this.top + 1) & kMask) === this.bottom
93
79
  }
@@ -101,9 +87,7 @@ class FixedCircularBuffer {
101
87
  this.top = (this.top + 1) & kMask
102
88
  }
103
89
 
104
- /**
105
- * @returns {T|null}
106
- */
90
+ /** @returns {T|null} */
107
91
  shift () {
108
92
  const nextItem = this.list[this.bottom]
109
93
  if (nextItem === undefined) { return null }
@@ -118,22 +102,16 @@ class FixedCircularBuffer {
118
102
  */
119
103
  module.exports = class FixedQueue {
120
104
  constructor () {
121
- /**
122
- * @type {FixedCircularBuffer<T>}
123
- */
105
+ /** @type {FixedCircularBuffer<T>} */
124
106
  this.head = this.tail = new FixedCircularBuffer()
125
107
  }
126
108
 
127
- /**
128
- * @returns {boolean}
129
- */
109
+ /** @returns {boolean} */
130
110
  isEmpty () {
131
111
  return this.head.isEmpty()
132
112
  }
133
113
 
134
- /**
135
- * @param {T} data
136
- */
114
+ /** @param {T} data */
137
115
  push (data) {
138
116
  if (this.head.isFull()) {
139
117
  // Head is full: Creates a new queue, sets the old queue's `.next` to it,
@@ -143,9 +121,7 @@ module.exports = class FixedQueue {
143
121
  this.head.push(data)
144
122
  }
145
123
 
146
- /**
147
- * @returns {T|null}
148
- */
124
+ /** @returns {T|null} */
149
125
  shift () {
150
126
  const tail = this.tail
151
127
  const next = tail.shift()
@@ -12,8 +12,6 @@ class H2CClient extends DispatcherBase {
12
12
  #client = null
13
13
 
14
14
  constructor (origin, clientOpts) {
15
- super()
16
-
17
15
  if (typeof origin === 'string') {
18
16
  origin = new URL(origin)
19
17
  }
@@ -47,6 +45,8 @@ class H2CClient extends DispatcherBase {
47
45
  )
48
46
  }
49
47
 
48
+ super()
49
+
50
50
  this.#client = new Client(origin, {
51
51
  ...opts,
52
52
  connect: this.#buildConnector(connect),
@@ -110,12 +110,12 @@ class H2CClient extends DispatcherBase {
110
110
  return this.#client.dispatch(opts, handler)
111
111
  }
112
112
 
113
- async [kClose] () {
114
- await this.#client.close()
113
+ [kClose] () {
114
+ return this.#client.close()
115
115
  }
116
116
 
117
- async [kDestroy] () {
118
- await this.#client.destroy()
117
+ [kDestroy] () {
118
+ return this.#client.destroy()
119
119
  }
120
120
  }
121
121