undici 7.3.0 → 7.5.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
@@ -337,7 +337,8 @@ See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-
337
337
 
338
338
  * dispatcher `Dispatcher`
339
339
 
340
- Sets the global dispatcher used by Common API Methods.
340
+ Sets the global dispatcher used by Common API Methods. Global dispatcher is shared among compatible undici modules,
341
+ including undici that is bundled internally with node.js.
341
342
 
342
343
  ### `undici.getGlobalDispatcher()`
343
344
 
@@ -210,7 +210,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
210
210
  * **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string []>, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
211
211
  * **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
212
212
  * **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
213
- * **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
213
+ * **onResponseError** `(controller: DispatchController, error: Error) => void` - Invoked when an error has occurred. May not throw.
214
214
 
215
215
  #### Example 1 - Dispatch GET request
216
216
 
@@ -1,7 +1,5 @@
1
1
  # Class: EnvHttpProxyAgent
2
2
 
3
- Stability: Experimental.
4
-
5
3
  Extends: `undici.Dispatcher`
6
4
 
7
5
  EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `http_proxy`, `https_proxy`, and `no_proxy` and sets up the proxy agents accordingly. When `http_proxy` and `https_proxy` are set, `http_proxy` is used for HTTP requests and `https_proxy` is used for HTTPS requests. If only `http_proxy` is set, `http_proxy` is used for both HTTP and HTTPS requests. If only `https_proxy` is set, it is only used for HTTPS requests.
@@ -28,6 +28,7 @@ import { errors } from 'undici'
28
28
  | `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
29
29
  | `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
30
30
 
31
+ Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === '<error_code>'` instead to avoid inconsistencies.
31
32
  ### `SocketError`
32
33
 
33
34
  The `SocketError` has a `.socket` property which holds socket metadata:
@@ -179,7 +179,9 @@ for await (const data of result2.body) {
179
179
  console.log('data', data.toString('utf8')) // data hello
180
180
  }
181
181
  ```
182
+
182
183
  #### Example - Mock different requests within the same file
184
+
183
185
  ```js
184
186
  const { MockAgent, setGlobalDispatcher } = require('undici');
185
187
  const agent = new MockAgent();
@@ -540,3 +542,60 @@ agent.assertNoPendingInterceptors()
540
542
  // │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │
541
543
  // └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
542
544
  ```
545
+
546
+ #### Example - access call history on MockAgent
547
+
548
+ You can register every call made within a MockAgent to be able to retrieve the body, headers and so on.
549
+
550
+ This is not enabled by default.
551
+
552
+ ```js
553
+ import { MockAgent, setGlobalDispatcher, request } from 'undici'
554
+
555
+ const mockAgent = new MockAgent({ enableCallHistory: true })
556
+ setGlobalDispatcher(mockAgent)
557
+
558
+ await request('http://example.com', { query: { item: 1 }})
559
+
560
+ mockAgent.getCallHistory()?.firstCall()
561
+ // Returns
562
+ // MockCallHistoryLog {
563
+ // body: undefined,
564
+ // headers: undefined,
565
+ // method: 'GET',
566
+ // origin: 'http://example.com',
567
+ // fullUrl: 'http://example.com/?item=1',
568
+ // path: '/',
569
+ // searchParams: { item: '1' },
570
+ // protocol: 'http:',
571
+ // host: 'example.com',
572
+ // port: ''
573
+ // }
574
+ ```
575
+
576
+ #### Example - clear call history
577
+
578
+ ```js
579
+ const mockAgent = new MockAgent()
580
+
581
+ mockAgent.clearAllCallHistory()
582
+ ```
583
+
584
+ #### Example - call history instance class method
585
+
586
+ ```js
587
+ const mockAgent = new MockAgent()
588
+
589
+ const mockAgentHistory = mockAgent.getCallHistory()
590
+
591
+ mockAgentHistory?.calls() // returns an array of MockCallHistoryLogs
592
+ mockAgentHistory?.firstCall() // returns the first MockCallHistoryLogs or undefined
593
+ mockAgentHistory?.lastCall() // returns the last MockCallHistoryLogs or undefined
594
+ mockAgentHistory?.nthCall(3) // returns the third MockCallHistoryLogs or undefined
595
+ mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint OR hash === #hash-value
596
+ mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }, { operator: 'AND' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint AND hash === #hash-value
597
+ mockAgentHistory?.filterCalls(/"data": "{}"/) // returns an Array of MockCallHistoryLogs where any value match regexp
598
+ mockAgentHistory?.filterCalls('application/json') // returns an Array of MockCallHistoryLogs where any value === 'application/json'
599
+ mockAgentHistory?.filterCalls((log) => log.path === '/endpoint') // returns an Array of MockCallHistoryLogs when given function returns true
600
+ mockAgentHistory?.clear() // clear the history
601
+ ```
@@ -0,0 +1,197 @@
1
+ # Class: MockCallHistory
2
+
3
+ Access to an instance with :
4
+
5
+ ```js
6
+ const mockAgent = new MockAgent({ enableCallHistory: true })
7
+ mockAgent.getCallHistory()
8
+
9
+ // or
10
+ const mockAgent = new MockAgent()
11
+ mockAgent.enableMockHistory()
12
+ mockAgent.getCallHistory()
13
+
14
+ ```
15
+
16
+ a MockCallHistory instance implements a **Symbol.iterator** letting you iterate on registered logs :
17
+
18
+ ```ts
19
+ for (const log of mockAgent.getCallHistory()) {
20
+ //...
21
+ }
22
+
23
+ const array: Array<MockCallHistoryLog> = [...mockAgent.getCallHistory()]
24
+ const set: Set<MockCallHistoryLog> = new Set(mockAgent.getCallHistory())
25
+ ```
26
+
27
+ ## class methods
28
+
29
+ ### clear
30
+
31
+ Clear all MockCallHistoryLog registered. This is automatically done when calling `mockAgent.close()`
32
+
33
+ ```js
34
+ mockAgent.clearCallHistory()
35
+ // same as
36
+ mockAgent.getCallHistory()?.clear()
37
+ ```
38
+
39
+ ### calls
40
+
41
+ Get all MockCallHistoryLog registered as an array
42
+
43
+ ```js
44
+ mockAgent.getCallHistory()?.calls()
45
+ ```
46
+
47
+ ### firstCall
48
+
49
+ Get the first MockCallHistoryLog registered or undefined
50
+
51
+ ```js
52
+ mockAgent.getCallHistory()?.firstCall()
53
+ ```
54
+
55
+ ### lastCall
56
+
57
+ Get the last MockCallHistoryLog registered or undefined
58
+
59
+ ```js
60
+ mockAgent.getCallHistory()?.lastCall()
61
+ ```
62
+
63
+ ### nthCall
64
+
65
+ Get the nth MockCallHistoryLog registered or undefined
66
+
67
+ ```js
68
+ mockAgent.getCallHistory()?.nthCall(3) // the third MockCallHistoryLog registered
69
+ ```
70
+
71
+ ### filterCallsByProtocol
72
+
73
+ Filter MockCallHistoryLog by protocol.
74
+
75
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
76
+
77
+ ```js
78
+ mockAgent.getCallHistory()?.filterCallsByProtocol(/https/)
79
+ mockAgent.getCallHistory()?.filterCallsByProtocol('https:')
80
+ ```
81
+
82
+ ### filterCallsByHost
83
+
84
+ Filter MockCallHistoryLog by host.
85
+
86
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
87
+
88
+ ```js
89
+ mockAgent.getCallHistory()?.filterCallsByHost(/localhost/)
90
+ mockAgent.getCallHistory()?.filterCallsByHost('localhost:3000')
91
+ ```
92
+
93
+ ### filterCallsByPort
94
+
95
+ Filter MockCallHistoryLog by port.
96
+
97
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
98
+
99
+ ```js
100
+ mockAgent.getCallHistory()?.filterCallsByPort(/3000/)
101
+ mockAgent.getCallHistory()?.filterCallsByPort('3000')
102
+ mockAgent.getCallHistory()?.filterCallsByPort('')
103
+ ```
104
+
105
+ ### filterCallsByOrigin
106
+
107
+ Filter MockCallHistoryLog by origin.
108
+
109
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
110
+
111
+ ```js
112
+ mockAgent.getCallHistory()?.filterCallsByOrigin(/http:\/\/localhost:3000/)
113
+ mockAgent.getCallHistory()?.filterCallsByOrigin('http://localhost:3000')
114
+ ```
115
+
116
+ ### filterCallsByPath
117
+
118
+ Filter MockCallHistoryLog by path.
119
+
120
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
121
+
122
+ ```js
123
+ mockAgent.getCallHistory()?.filterCallsByPath(/api\/v1\/graphql/)
124
+ mockAgent.getCallHistory()?.filterCallsByPath('/api/v1/graphql')
125
+ ```
126
+
127
+ ### filterCallsByHash
128
+
129
+ Filter MockCallHistoryLog by hash.
130
+
131
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
132
+
133
+ ```js
134
+ mockAgent.getCallHistory()?.filterCallsByPath(/hash/)
135
+ mockAgent.getCallHistory()?.filterCallsByPath('#hash')
136
+ ```
137
+
138
+ ### filterCallsByFullUrl
139
+
140
+ Filter MockCallHistoryLog by fullUrl. fullUrl contains protocol, host, port, path, hash, and query params
141
+
142
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
143
+
144
+ ```js
145
+ mockAgent.getCallHistory()?.filterCallsByFullUrl(/https:\/\/localhost:3000\/\?query=value#hash/)
146
+ mockAgent.getCallHistory()?.filterCallsByFullUrl('https://localhost:3000/?query=value#hash')
147
+ ```
148
+
149
+ ### filterCallsByMethod
150
+
151
+ Filter MockCallHistoryLog by method.
152
+
153
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
154
+
155
+ ```js
156
+ mockAgent.getCallHistory()?.filterCallsByMethod(/POST/)
157
+ mockAgent.getCallHistory()?.filterCallsByMethod('POST')
158
+ ```
159
+
160
+ ### filterCalls
161
+
162
+ This class method is a meta function / alias to apply complex filtering in a single way.
163
+
164
+ Parameters :
165
+
166
+ - criteria : the first parameter. a function, regexp or object.
167
+ - function : filter MockCallHistoryLog when the function returns false
168
+ - regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](./MockCallHistoryLog.md#to-string))
169
+ - object : an object with MockCallHistoryLog properties as keys to apply multiple filters. each values are a [filter parameter](/docs/docs/api/MockCallHistory.md#filter-parameter)
170
+ - options : the second parameter. an object.
171
+ - options.operator : `'AND'` or `'OR'` (default `'OR'`). Used only if criteria is an object. see below
172
+
173
+ ```js
174
+ mockAgent.getCallHistory()?.filterCalls((log) => log.hash === value && log.headers?.['authorization'] !== undefined)
175
+ mockAgent.getCallHistory()?.filterCalls(/"data": "{ "errors": "wrong body" }"/)
176
+
177
+ // returns an Array of MockCallHistoryLog which all have
178
+ // - a hash containing my-hash
179
+ // - OR
180
+ // - a path equal to /endpoint
181
+ mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' })
182
+
183
+ // returns an Array of MockCallHistoryLog which all have
184
+ // - a hash containing my-hash
185
+ // - AND
186
+ // - a path equal to /endpoint
187
+ mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' }, { operator: 'AND' })
188
+ ```
189
+
190
+ ## filter parameter
191
+
192
+ Can be :
193
+
194
+ - string. MockCallHistoryLog filtered if `value !== parameterValue`
195
+ - null. MockCallHistoryLog filtered if `value !== parameterValue`
196
+ - undefined. MockCallHistoryLog filtered if `value !== parameterValue`
197
+ - regexp. MockCallHistoryLog filtered if `!parameterValue.test(value)`
@@ -0,0 +1,43 @@
1
+ # Class: MockCallHistoryLog
2
+
3
+ Access to an instance with :
4
+
5
+ ```js
6
+ const mockAgent = new MockAgent({ enableCallHistory: true })
7
+ mockAgent.getCallHistory()?.firstCall()
8
+ ```
9
+
10
+ ## class properties
11
+
12
+ - body `mockAgent.getCallHistory()?.firstCall()?.body`
13
+ - headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
14
+ - method `mockAgent.getCallHistory()?.firstCall()?.method` a string
15
+ - fullUrl `mockAgent.getCallHistory()?.firstCall()?.fullUrl` a string containing the protocol, origin, path, query and hash
16
+ - origin `mockAgent.getCallHistory()?.firstCall()?.origin` a string containing the protocol and the host
17
+ - headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
18
+ - path `mockAgent.getCallHistory()?.firstCall()?.path` a string always starting with `/`
19
+ - searchParams `mockAgent.getCallHistory()?.firstCall()?.searchParams` an object
20
+ - protocol `mockAgent.getCallHistory()?.firstCall()?.protocol` a string (`https:`)
21
+ - host `mockAgent.getCallHistory()?.firstCall()?.host` a string
22
+ - port `mockAgent.getCallHistory()?.firstCall()?.port` an empty string or a string containing numbers
23
+ - hash `mockAgent.getCallHistory()?.firstCall()?.hash` an empty string or a string starting with `#`
24
+
25
+ ## class methods
26
+
27
+ ### toMap
28
+
29
+ Returns a Map instance
30
+
31
+ ```js
32
+ mockAgent.getCallHistory()?.firstCall()?.toMap()?.get('hash')
33
+ // #hash
34
+ ```
35
+
36
+ ### toString
37
+
38
+ Returns a string computed with any class property name and value pair
39
+
40
+ ```js
41
+ mockAgent.getCallHistory()?.firstCall()?.toString()
42
+ // protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->{"content-type":"application/json"}
43
+ ```
@@ -29,7 +29,7 @@ And this is what the test file looks like:
29
29
 
30
30
  ```js
31
31
  // index.test.mjs
32
- import { strict as assert } from 'assert'
32
+ import { strict as assert } from 'node:assert'
33
33
  import { MockAgent, setGlobalDispatcher, } from 'undici'
34
34
  import { bankTransfer } from './bank.mjs'
35
35
 
@@ -75,6 +75,60 @@ assert.deepEqual(badRequest, { message: 'bank account not found' })
75
75
 
76
76
  Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
77
77
 
78
+ ## Access agent call history
79
+
80
+ Using a MockAgent also allows you to make assertions on the configuration used to make your request in your application.
81
+
82
+ Here is an example :
83
+
84
+ ```js
85
+ // index.test.mjs
86
+ import { strict as assert } from 'node:assert'
87
+ import { MockAgent, setGlobalDispatcher, fetch } from 'undici'
88
+ import { app } from './app.mjs'
89
+
90
+ // given an application server running on http://localhost:3000
91
+ await app.start()
92
+
93
+ // enable call history at instantiation
94
+ const mockAgent = new MockAgent({ enableCallHistory: true })
95
+ // or after instantiation
96
+ mockAgent.enableCallHistory()
97
+
98
+ setGlobalDispatcher(mockAgent)
99
+
100
+ // this call is made (not intercepted)
101
+ await fetch(`http://localhost:3000/endpoint?query='hello'`, {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'application/json' }
104
+ body: JSON.stringify({ data: '' })
105
+ })
106
+
107
+ // access to the call history of the MockAgent (which register every call made intercepted or not)
108
+ assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
109
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.fullUrl, `http://localhost:3000/endpoint?query='hello'`)
110
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.body, JSON.stringify({ data: '' }))
111
+ assert.deepStrictEqual(mockAgent.getCallHistory()?.firstCall()?.searchParams, { query: 'hello' })
112
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.port, '3000')
113
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.host, 'localhost:3000')
114
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.method, 'POST')
115
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.path, '/endpoint')
116
+ assert.deepStrictEqual(mockAgent.getCallHistory()?.firstCall()?.headers, { 'content-type': 'application/json' })
117
+
118
+ // clear all call history logs
119
+ mockAgent.clearCallHistory()
120
+
121
+ assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
122
+ ```
123
+
124
+ Calling `mockAgent.close()` will automatically clear and delete every call history for you.
125
+
126
+ Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
127
+
128
+ Explore other MockCallHistory functionality [here](/docs/docs/api/MockCallHistory.md)
129
+
130
+ Explore other MockCallHistoryLog functionality [here](/docs/docs/api/MockCallHistoryLog.md)
131
+
78
132
  ## Debug Mock Value
79
133
 
80
134
  When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
package/index-fetch.js CHANGED
@@ -26,6 +26,9 @@ module.exports.createFastMessageEvent = createFastMessageEvent
26
26
 
27
27
  module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource
28
28
 
29
+ const api = require('./lib/api')
30
+ const Dispatcher = require('./lib/dispatcher/dispatcher')
31
+ Object.assign(Dispatcher.prototype, api)
29
32
  // Expose the fetch implementation to be enabled in Node.js core via a flag
30
33
  module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent
31
34
  module.exports.getGlobalDispatcher = getGlobalDispatcher
package/index.js CHANGED
@@ -14,6 +14,7 @@ const { InvalidArgumentError } = errors
14
14
  const api = require('./lib/api')
15
15
  const buildConnector = require('./lib/core/connect')
16
16
  const MockClient = require('./lib/mock/mock-client')
17
+ const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
17
18
  const MockAgent = require('./lib/mock/mock-agent')
18
19
  const MockPool = require('./lib/mock/mock-pool')
19
20
  const mockErrors = require('./lib/mock/mock-errors')
@@ -169,6 +170,8 @@ module.exports.connect = makeDispatcher(api.connect)
169
170
  module.exports.upgrade = makeDispatcher(api.upgrade)
170
171
 
171
172
  module.exports.MockClient = MockClient
173
+ module.exports.MockCallHistory = MockCallHistory
174
+ module.exports.MockCallHistoryLog = MockCallHistoryLog
172
175
  module.exports.MockPool = MockPool
173
176
  module.exports.MockAgent = MockAgent
174
177
  module.exports.mockErrors = mockErrors
@@ -79,7 +79,13 @@ class MemoryCacheStore {
79
79
  const entry = this.#entries.get(topLevelKey)?.find((entry) => (
80
80
  entry.deleteAt > now &&
81
81
  entry.method === key.method &&
82
- (entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers?.[headerName]))
82
+ (entry.vary == null || Object.keys(entry.vary).every(headerName => {
83
+ if (entry.vary[headerName] === null) {
84
+ return key.headers[headerName] === undefined
85
+ }
86
+
87
+ return entry.vary[headerName] === key.headers[headerName]
88
+ }))
83
89
  ))
84
90
 
85
91
  return entry == null
@@ -232,7 +232,7 @@ module.exports = class SqliteCacheStore {
232
232
  const value = this.#findValue(key)
233
233
  return value
234
234
  ? {
235
- body: value.body ? Buffer.from(value.body.buffer) : undefined,
235
+ body: value.body ? Buffer.from(value.body.buffer, value.body.byteOffset, value.body.byteLength) : undefined,
236
236
  statusCode: value.statusCode,
237
237
  statusMessage: value.statusMessage,
238
238
  headers: value.headers ? JSON.parse(value.headers) : undefined,
@@ -411,10 +411,6 @@ module.exports = class SqliteCacheStore {
411
411
  let matches = true
412
412
 
413
413
  if (value.vary) {
414
- if (!headers) {
415
- return undefined
416
- }
417
-
418
414
  const vary = JSON.parse(value.vary)
419
415
 
420
416
  for (const header in vary) {
@@ -440,18 +436,21 @@ module.exports = class SqliteCacheStore {
440
436
  * @returns {boolean}
441
437
  */
442
438
  function headerValueEquals (lhs, rhs) {
439
+ if (lhs == null && rhs == null) {
440
+ return true
441
+ }
442
+
443
+ if ((lhs == null && rhs != null) ||
444
+ (lhs != null && rhs == null)) {
445
+ return false
446
+ }
447
+
443
448
  if (Array.isArray(lhs) && Array.isArray(rhs)) {
444
449
  if (lhs.length !== rhs.length) {
445
450
  return false
446
451
  }
447
452
 
448
- for (let i = 0; i < lhs.length; i++) {
449
- if (rhs.includes(lhs[i])) {
450
- return false
451
- }
452
- }
453
-
454
- return true
453
+ return lhs.every((x, i) => x === rhs[i])
455
454
  }
456
455
 
457
456
  return lhs === rhs
@@ -207,7 +207,7 @@ class Client extends DispatcherBase {
207
207
  allowH2,
208
208
  socketPath,
209
209
  timeout: connectTimeout,
210
- ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
210
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
211
211
  ...connect
212
212
  })
213
213
  }
@@ -10,8 +10,6 @@ const DEFAULT_PORTS = {
10
10
  'https:': 443
11
11
  }
12
12
 
13
- let experimentalWarned = false
14
-
15
13
  class EnvHttpProxyAgent extends DispatcherBase {
16
14
  #noProxyValue = null
17
15
  #noProxyEntries = null
@@ -21,13 +19,6 @@ class EnvHttpProxyAgent extends DispatcherBase {
21
19
  super()
22
20
  this.#opts = opts
23
21
 
24
- if (!experimentalWarned) {
25
- experimentalWarned = true
26
- process.emitWarning('EnvHttpProxyAgent is experimental, expect them to change at any time.', {
27
- code: 'UNDICI-EHPA'
28
- })
29
- }
30
-
31
22
  const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
32
23
 
33
24
  this[kNoProxyAgent] = new Agent(agentOpts)
@@ -58,7 +58,7 @@ class Pool extends PoolBase {
58
58
  allowH2,
59
59
  socketPath,
60
60
  timeout: connectTimeout,
61
- ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
61
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
62
62
  ...connect
63
63
  })
64
64
  }
@@ -70,6 +70,20 @@ class Pool extends PoolBase {
70
70
  ? { ...options.interceptors }
71
71
  : undefined
72
72
  this[kFactory] = factory
73
+
74
+ this.on('connectionError', (origin, targets, error) => {
75
+ // If a connection error occurs, we remove the client from the pool,
76
+ // and emit a connectionError event. They will not be re-used.
77
+ // Fixes https://github.com/nodejs/undici/issues/3895
78
+ for (const target of targets) {
79
+ // Do not use kRemoveClient here, as it will close the client,
80
+ // but the client cannot be closed in this state.
81
+ const idx = this[kClients].indexOf(target)
82
+ if (idx !== -1) {
83
+ this[kClients].splice(idx, 1)
84
+ }
85
+ }
86
+ })
73
87
  }
74
88
 
75
89
  [kGetDispatcher] () {
@@ -11,8 +11,8 @@ const {
11
11
  } = require('../core/util')
12
12
 
13
13
  function calculateRetryAfterHeader (retryAfter) {
14
- const current = Date.now()
15
- return new Date(retryAfter).getTime() - current
14
+ const retryTime = new Date(retryAfter).getTime()
15
+ return isNaN(retryTime) ? 0 : retryTime - Date.now()
16
16
  }
17
17
 
18
18
  class RetryHandler {
@@ -124,7 +124,7 @@ class RetryHandler {
124
124
  if (retryAfterHeader) {
125
125
  retryAfterHeader = Number(retryAfterHeader)
126
126
  retryAfterHeader = Number.isNaN(retryAfterHeader)
127
- ? calculateRetryAfterHeader(retryAfterHeader)
127
+ ? calculateRetryAfterHeader(headers['retry-after'])
128
128
  : retryAfterHeader * 1e3 // Retry-After is in seconds
129
129
  }
130
130