undici 7.8.0 → 7.10.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 +20 -15
- package/docs/docs/api/Agent.md +6 -0
- package/docs/docs/api/CacheStore.md +22 -2
- package/docs/docs/api/ClientStats.md +27 -0
- package/docs/docs/api/DiagnosticsChannel.md +1 -1
- package/docs/docs/api/MockAgent.md +2 -0
- package/docs/docs/api/Pool.md +1 -0
- package/docs/docs/api/ProxyAgent.md +1 -0
- package/lib/cache/memory-cache-store.js +64 -13
- package/lib/dispatcher/agent.js +33 -13
- package/lib/dispatcher/client-h2.js +3 -1
- package/lib/dispatcher/client.js +5 -0
- package/lib/dispatcher/pool-base.js +2 -5
- package/lib/dispatcher/pool.js +17 -3
- package/lib/dispatcher/proxy-agent.js +88 -2
- package/lib/interceptor/cache.js +12 -2
- package/lib/mock/mock-agent.js +22 -10
- package/lib/mock/mock-symbols.js +1 -0
- package/lib/mock/mock-utils.js +37 -3
- package/lib/util/cache.js +1 -3
- package/lib/util/stats.js +32 -0
- package/package.json +1 -1
- package/types/agent.d.ts +4 -0
- package/types/client-stats.d.ts +15 -0
- package/types/client.d.ts +6 -3
- package/types/mock-agent.d.ts +3 -0
- package/types/pool.d.ts +2 -0
- package/types/proxy-agent.d.ts +1 -0
- package/lib/dispatcher/pool-stats.js +0 -36
package/README.md
CHANGED
|
@@ -261,12 +261,22 @@ const readableWebStream = response.body
|
|
|
261
261
|
const readableNodeStream = Readable.fromWeb(readableWebStream)
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
## Specification Compliance
|
|
265
265
|
|
|
266
|
-
This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
|
|
266
|
+
This section documents parts of the [HTTP/1.1](https://www.rfc-editor.org/rfc/rfc9110.html) and [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
|
|
267
267
|
not support or does not fully implement.
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
#### CORS
|
|
270
|
+
|
|
271
|
+
Unlike browsers, Undici does not implement CORS (Cross-Origin Resource Sharing) checks by default. This means:
|
|
272
|
+
|
|
273
|
+
- No preflight requests are automatically sent for cross-origin requests
|
|
274
|
+
- No validation of `Access-Control-Allow-Origin` headers is performed
|
|
275
|
+
- Requests to any origin are allowed regardless of the source
|
|
276
|
+
|
|
277
|
+
This behavior is intentional for server-side environments where CORS restrictions are typically unnecessary. If your application requires CORS-like protections, you will need to implement these checks manually.
|
|
278
|
+
|
|
279
|
+
#### Garbage Collection
|
|
270
280
|
|
|
271
281
|
* https://fetch.spec.whatwg.org/#garbage-collection
|
|
272
282
|
|
|
@@ -307,7 +317,7 @@ const headers = await fetch(url, { method: 'HEAD' })
|
|
|
307
317
|
.then(res => res.headers)
|
|
308
318
|
```
|
|
309
319
|
|
|
310
|
-
|
|
320
|
+
#### Forbidden and Safelisted Header Names
|
|
311
321
|
|
|
312
322
|
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
|
|
313
323
|
* https://fetch.spec.whatwg.org/#forbidden-header-name
|
|
@@ -316,7 +326,7 @@ const headers = await fetch(url, { method: 'HEAD' })
|
|
|
316
326
|
|
|
317
327
|
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
|
|
318
328
|
|
|
319
|
-
|
|
329
|
+
#### `undici.upgrade([url, options]): Promise`
|
|
320
330
|
|
|
321
331
|
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
|
|
322
332
|
|
|
@@ -378,12 +388,7 @@ Returns: `URL`
|
|
|
378
388
|
* **protocol** `string` (optional)
|
|
379
389
|
* **search** `string` (optional)
|
|
380
390
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
This section documents parts of the HTTP/1.1 specification that Undici does
|
|
384
|
-
not support or does not fully implement.
|
|
385
|
-
|
|
386
|
-
### Expect
|
|
391
|
+
#### Expect
|
|
387
392
|
|
|
388
393
|
Undici does not support the `Expect` request header field. The request
|
|
389
394
|
body is always immediately sent and the `100 Continue` response will be
|
|
@@ -391,7 +396,7 @@ ignored.
|
|
|
391
396
|
|
|
392
397
|
Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
|
|
393
398
|
|
|
394
|
-
|
|
399
|
+
#### Pipelining
|
|
395
400
|
|
|
396
401
|
Undici will only use pipelining if configured with a `pipelining` factor
|
|
397
402
|
greater than `1`. Also it is important to pass `blocking: false` to the
|
|
@@ -412,7 +417,7 @@ aborted.
|
|
|
412
417
|
* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
|
|
413
418
|
* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
|
|
414
419
|
|
|
415
|
-
|
|
420
|
+
#### Manual Redirect
|
|
416
421
|
|
|
417
422
|
Since it is not possible to manually follow an HTTP redirect on the server-side,
|
|
418
423
|
Undici returns the actual response instead of an `opaqueredirect` filtered one
|
|
@@ -421,9 +426,9 @@ implementations in Deno and Cloudflare Workers.
|
|
|
421
426
|
|
|
422
427
|
Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
|
|
423
428
|
|
|
424
|
-
|
|
429
|
+
### Workarounds
|
|
425
430
|
|
|
426
|
-
|
|
431
|
+
#### Network address family autoselection.
|
|
427
432
|
|
|
428
433
|
If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
|
|
429
434
|
first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
|
package/docs/docs/api/Agent.md
CHANGED
|
@@ -75,3 +75,9 @@ See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatche
|
|
|
75
75
|
### `Agent.upgrade(options[, callback])`
|
|
76
76
|
|
|
77
77
|
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
|
78
|
+
|
|
79
|
+
### `Agent.stats()`
|
|
80
|
+
|
|
81
|
+
Returns an object of stats by origin in the format of `Record<string, TClientStats | TPoolStats>`
|
|
82
|
+
|
|
83
|
+
See [`PoolStats`](/docs/docs/api/PoolStats.md) and [`ClientStats`](/docs/docs/api/ClientStats.md).
|
|
@@ -13,8 +13,28 @@ The `MemoryCacheStore` stores the responses in-memory.
|
|
|
13
13
|
|
|
14
14
|
**Options**
|
|
15
15
|
|
|
16
|
+
- `maxSize` - The maximum total size in bytes of all stored responses. Default `Infinity`.
|
|
16
17
|
- `maxCount` - The maximum amount of responses to store. Default `Infinity`.
|
|
17
|
-
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.
|
|
18
|
+
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
|
|
19
|
+
|
|
20
|
+
### Getters
|
|
21
|
+
|
|
22
|
+
#### `MemoryCacheStore.size`
|
|
23
|
+
|
|
24
|
+
Returns the current total size in bytes of all stored responses.
|
|
25
|
+
|
|
26
|
+
### Methods
|
|
27
|
+
|
|
28
|
+
#### `MemoryCacheStore.isFull()`
|
|
29
|
+
|
|
30
|
+
Returns a boolean indicating whether the cache has reached its maximum size or count.
|
|
31
|
+
|
|
32
|
+
### Events
|
|
33
|
+
|
|
34
|
+
#### `'maxSizeExceeded'`
|
|
35
|
+
|
|
36
|
+
Emitted when the cache exceeds its maximum size or count limits. The event payload contains `size`, `maxSize`, `count`, and `maxCount` properties.
|
|
37
|
+
|
|
18
38
|
|
|
19
39
|
### `SqliteCacheStore`
|
|
20
40
|
|
|
@@ -26,7 +46,7 @@ The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present.
|
|
|
26
46
|
|
|
27
47
|
- `location` - The location of the SQLite database to use. Default `:memory:`.
|
|
28
48
|
- `maxCount` - The maximum number of entries to store in the database. Default `Infinity`.
|
|
29
|
-
- `maxEntrySize` - The maximum size in bytes that a
|
|
49
|
+
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
|
|
30
50
|
|
|
31
51
|
## Defining a Custom Cache Store
|
|
32
52
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Class: ClientStats
|
|
2
|
+
|
|
3
|
+
Stats for a [Client](/docs/docs/api/Client.md).
|
|
4
|
+
|
|
5
|
+
## `new ClientStats(client)`
|
|
6
|
+
|
|
7
|
+
Arguments:
|
|
8
|
+
|
|
9
|
+
* **client** `Client` - Client from which to return stats.
|
|
10
|
+
|
|
11
|
+
## Instance Properties
|
|
12
|
+
|
|
13
|
+
### `ClientStats.connected`
|
|
14
|
+
|
|
15
|
+
Boolean if socket as open connection by this client.
|
|
16
|
+
|
|
17
|
+
### `ClientStats.pending`
|
|
18
|
+
|
|
19
|
+
Number of pending requests of this client.
|
|
20
|
+
|
|
21
|
+
### `ClientStats.running`
|
|
22
|
+
|
|
23
|
+
Number of currently active requests across this client.
|
|
24
|
+
|
|
25
|
+
### `ClientStats.size`
|
|
26
|
+
|
|
27
|
+
Number of active, pending, or queued requests of this clients.
|
|
@@ -19,7 +19,7 @@ diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
|
|
|
19
19
|
console.log('completed', request.completed)
|
|
20
20
|
console.log('method', request.method)
|
|
21
21
|
console.log('path', request.path)
|
|
22
|
-
console.log('headers') // array of strings, e.g: ['foo', 'bar']
|
|
22
|
+
console.log('headers', request.headers) // array of strings, e.g: ['foo', 'bar']
|
|
23
23
|
request.addHeader('hello', 'world')
|
|
24
24
|
console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world']
|
|
25
25
|
})
|
|
@@ -20,6 +20,8 @@ Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
|
|
|
20
20
|
|
|
21
21
|
* **ignoreTrailingSlash** `boolean` (optional) - Default: `false` - set the default value for `ignoreTrailingSlash` for interceptors.
|
|
22
22
|
|
|
23
|
+
* **acceptNonStandardSearchParameters** `boolean` (optional) - Default: `false` - set to `true` if the matcher should also accept non standard search parameters such as multi-value items specified with `[]` (e.g. `param[]=1¶m[]=2¶m[]=3`) and multi-value items which values are comma separated (e.g. `param=1,2,3`).
|
|
24
|
+
|
|
23
25
|
### Example - Basic MockAgent instantiation
|
|
24
26
|
|
|
25
27
|
This will instantiate the MockAgent. It will not do anything until registered as the agent to use with requests and mock interceptions are added.
|
package/docs/docs/api/Pool.md
CHANGED
|
@@ -19,6 +19,7 @@ Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions)
|
|
|
19
19
|
|
|
20
20
|
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
|
|
21
21
|
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
|
|
22
|
+
* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `Pool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age.
|
|
22
23
|
|
|
23
24
|
## Instance Properties
|
|
24
25
|
|
|
@@ -25,6 +25,7 @@ For detailed information on the parsing process and potential validation errors,
|
|
|
25
25
|
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
|
|
26
26
|
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
|
27
27
|
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
|
28
|
+
* **proxyTunnel** `boolean` (optional) - By default, ProxyAgent will request that the Proxy facilitate a tunnel between the endpoint and the agent. Setting `proxyTunnel` to false avoids issuing a CONNECT extension, and includes the endpoint domain and path in each request.
|
|
28
29
|
|
|
29
30
|
Examples:
|
|
30
31
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Writable } = require('node:stream')
|
|
4
|
+
const { EventEmitter } = require('node:events')
|
|
4
5
|
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -12,8 +13,9 @@ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @implements {CacheStore}
|
|
16
|
+
* @extends {EventEmitter}
|
|
15
17
|
*/
|
|
16
|
-
class MemoryCacheStore {
|
|
18
|
+
class MemoryCacheStore extends EventEmitter {
|
|
17
19
|
#maxCount = Infinity
|
|
18
20
|
#maxSize = Infinity
|
|
19
21
|
#maxEntrySize = Infinity
|
|
@@ -21,11 +23,13 @@ class MemoryCacheStore {
|
|
|
21
23
|
#size = 0
|
|
22
24
|
#count = 0
|
|
23
25
|
#entries = new Map()
|
|
26
|
+
#hasEmittedMaxSizeEvent = false
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
|
|
27
30
|
*/
|
|
28
31
|
constructor (opts) {
|
|
32
|
+
super()
|
|
29
33
|
if (opts) {
|
|
30
34
|
if (typeof opts !== 'object') {
|
|
31
35
|
throw new TypeError('MemoryCacheStore options must be an object')
|
|
@@ -66,6 +70,22 @@ class MemoryCacheStore {
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Get the current size of the cache in bytes
|
|
75
|
+
* @returns {number} The current size of the cache in bytes
|
|
76
|
+
*/
|
|
77
|
+
get size () {
|
|
78
|
+
return this.#size
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if the cache is full (either max size or max count reached)
|
|
83
|
+
* @returns {boolean} True if the cache is full, false otherwise
|
|
84
|
+
*/
|
|
85
|
+
isFull () {
|
|
86
|
+
return this.#size >= this.#maxSize || this.#count >= this.#maxCount
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
/**
|
|
70
90
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
|
|
71
91
|
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
|
|
@@ -76,17 +96,9 @@ class MemoryCacheStore {
|
|
|
76
96
|
const topLevelKey = `${key.origin}:${key.path}`
|
|
77
97
|
|
|
78
98
|
const now = Date.now()
|
|
79
|
-
const
|
|
80
|
-
entry.deleteAt > now &&
|
|
81
|
-
entry.method === key.method &&
|
|
82
|
-
(entry.vary == null || Object.keys(entry.vary).every(headerName => {
|
|
83
|
-
if (entry.vary[headerName] === null) {
|
|
84
|
-
return key.headers[headerName] === undefined
|
|
85
|
-
}
|
|
99
|
+
const entries = this.#entries.get(topLevelKey)
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
}))
|
|
89
|
-
))
|
|
101
|
+
const entry = entries ? findEntry(key, entries, now) : null
|
|
90
102
|
|
|
91
103
|
return entry == null
|
|
92
104
|
? undefined
|
|
@@ -140,12 +152,32 @@ class MemoryCacheStore {
|
|
|
140
152
|
entries = []
|
|
141
153
|
store.#entries.set(topLevelKey, entries)
|
|
142
154
|
}
|
|
143
|
-
entries.
|
|
155
|
+
const previousEntry = findEntry(key, entries, Date.now())
|
|
156
|
+
if (previousEntry) {
|
|
157
|
+
const index = entries.indexOf(previousEntry)
|
|
158
|
+
entries.splice(index, 1, entry)
|
|
159
|
+
store.#size -= previousEntry.size
|
|
160
|
+
} else {
|
|
161
|
+
entries.push(entry)
|
|
162
|
+
store.#count += 1
|
|
163
|
+
}
|
|
144
164
|
|
|
145
165
|
store.#size += entry.size
|
|
146
|
-
store.#count += 1
|
|
147
166
|
|
|
167
|
+
// Check if cache is full and emit event if needed
|
|
148
168
|
if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
|
|
169
|
+
// Emit maxSizeExceeded event if we haven't already
|
|
170
|
+
if (!store.#hasEmittedMaxSizeEvent) {
|
|
171
|
+
store.emit('maxSizeExceeded', {
|
|
172
|
+
size: store.#size,
|
|
173
|
+
maxSize: store.#maxSize,
|
|
174
|
+
count: store.#count,
|
|
175
|
+
maxCount: store.#maxCount
|
|
176
|
+
})
|
|
177
|
+
store.#hasEmittedMaxSizeEvent = true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Perform eviction
|
|
149
181
|
for (const [key, entries] of store.#entries) {
|
|
150
182
|
for (const entry of entries.splice(0, entries.length / 2)) {
|
|
151
183
|
store.#size -= entry.size
|
|
@@ -155,6 +187,11 @@ class MemoryCacheStore {
|
|
|
155
187
|
store.#entries.delete(key)
|
|
156
188
|
}
|
|
157
189
|
}
|
|
190
|
+
|
|
191
|
+
// Reset the event flag after eviction
|
|
192
|
+
if (store.#size < store.#maxSize && store.#count < store.#maxCount) {
|
|
193
|
+
store.#hasEmittedMaxSizeEvent = false
|
|
194
|
+
}
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
callback(null)
|
|
@@ -180,4 +217,18 @@ class MemoryCacheStore {
|
|
|
180
217
|
}
|
|
181
218
|
}
|
|
182
219
|
|
|
220
|
+
function findEntry (key, entries, now) {
|
|
221
|
+
return entries.find((entry) => (
|
|
222
|
+
entry.deleteAt > now &&
|
|
223
|
+
entry.method === key.method &&
|
|
224
|
+
(entry.vary == null || Object.keys(entry.vary).every(headerName => {
|
|
225
|
+
if (entry.vary[headerName] === null) {
|
|
226
|
+
return key.headers[headerName] === undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return entry.vary[headerName] === key.headers[headerName]
|
|
230
|
+
}))
|
|
231
|
+
))
|
|
232
|
+
}
|
|
233
|
+
|
|
183
234
|
module.exports = MemoryCacheStore
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { InvalidArgumentError } = require('../core/errors')
|
|
4
|
-
const { kClients, kRunning, kClose, kDestroy, kDispatch } = require('../core/symbols')
|
|
4
|
+
const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
|
|
5
5
|
const DispatcherBase = require('./dispatcher-base')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const Client = require('./client')
|
|
@@ -45,22 +45,35 @@ class Agent extends DispatcherBase {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
this[kOnConnect] = (origin, targets) => {
|
|
48
|
+
const result = this[kClients].get(origin)
|
|
49
|
+
if (result) {
|
|
50
|
+
result.count += 1
|
|
51
|
+
}
|
|
48
52
|
this.emit('connect', origin, [this, ...targets])
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
this[kOnDisconnect] = (origin, targets, err) => {
|
|
56
|
+
const result = this[kClients].get(origin)
|
|
57
|
+
if (result) {
|
|
58
|
+
result.count -= 1
|
|
59
|
+
if (result.count <= 0) {
|
|
60
|
+
this[kClients].delete(origin)
|
|
61
|
+
result.dispatcher.destroy()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
52
64
|
this.emit('disconnect', origin, [this, ...targets], err)
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
this[kOnConnectionError] = (origin, targets, err) => {
|
|
68
|
+
// TODO: should this decrement result.count here?
|
|
56
69
|
this.emit('connectionError', origin, [this, ...targets], err)
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
get [kRunning] () {
|
|
61
74
|
let ret = 0
|
|
62
|
-
for (const
|
|
63
|
-
ret +=
|
|
75
|
+
for (const { dispatcher } of this[kClients].values()) {
|
|
76
|
+
ret += dispatcher[kRunning]
|
|
64
77
|
}
|
|
65
78
|
return ret
|
|
66
79
|
}
|
|
@@ -73,8 +86,8 @@ class Agent extends DispatcherBase {
|
|
|
73
86
|
throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
const result = this[kClients].get(key)
|
|
90
|
+
let dispatcher = result && result.dispatcher
|
|
78
91
|
if (!dispatcher) {
|
|
79
92
|
dispatcher = this[kFactory](opts.origin, this[kOptions])
|
|
80
93
|
.on('drain', this[kOnDrain])
|
|
@@ -82,10 +95,7 @@ class Agent extends DispatcherBase {
|
|
|
82
95
|
.on('disconnect', this[kOnDisconnect])
|
|
83
96
|
.on('connectionError', this[kOnConnectionError])
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
// TODO(mcollina): remove te timer when the client/pool do not have any more
|
|
87
|
-
// active connections.
|
|
88
|
-
this[kClients].set(key, dispatcher)
|
|
98
|
+
this[kClients].set(key, { count: 0, dispatcher })
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
return dispatcher.dispatch(opts, handler)
|
|
@@ -93,8 +103,8 @@ class Agent extends DispatcherBase {
|
|
|
93
103
|
|
|
94
104
|
async [kClose] () {
|
|
95
105
|
const closePromises = []
|
|
96
|
-
for (const
|
|
97
|
-
closePromises.push(
|
|
106
|
+
for (const { dispatcher } of this[kClients].values()) {
|
|
107
|
+
closePromises.push(dispatcher.close())
|
|
98
108
|
}
|
|
99
109
|
this[kClients].clear()
|
|
100
110
|
|
|
@@ -103,13 +113,23 @@ class Agent extends DispatcherBase {
|
|
|
103
113
|
|
|
104
114
|
async [kDestroy] (err) {
|
|
105
115
|
const destroyPromises = []
|
|
106
|
-
for (const
|
|
107
|
-
destroyPromises.push(
|
|
116
|
+
for (const { dispatcher } of this[kClients].values()) {
|
|
117
|
+
destroyPromises.push(dispatcher.destroy(err))
|
|
108
118
|
}
|
|
109
119
|
this[kClients].clear()
|
|
110
120
|
|
|
111
121
|
await Promise.all(destroyPromises)
|
|
112
122
|
}
|
|
123
|
+
|
|
124
|
+
get stats () {
|
|
125
|
+
const allClientStats = {}
|
|
126
|
+
for (const { dispatcher } of this[kClients].values()) {
|
|
127
|
+
if (dispatcher.stats) {
|
|
128
|
+
allClientStats[dispatcher[kUrl].origin] = dispatcher.stats
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return allClientStats
|
|
132
|
+
}
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
module.exports = Agent
|
|
@@ -295,11 +295,13 @@ function writeH2 (client, request) {
|
|
|
295
295
|
if (Array.isArray(val)) {
|
|
296
296
|
for (let i = 0; i < val.length; i++) {
|
|
297
297
|
if (headers[key]) {
|
|
298
|
-
headers[key] +=
|
|
298
|
+
headers[key] += `, ${val[i]}`
|
|
299
299
|
} else {
|
|
300
300
|
headers[key] = val[i]
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
+
} else if (headers[key]) {
|
|
304
|
+
headers[key] += `, ${val}`
|
|
303
305
|
} else {
|
|
304
306
|
headers[key] = val
|
|
305
307
|
}
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -4,6 +4,7 @@ const assert = require('node:assert')
|
|
|
4
4
|
const net = require('node:net')
|
|
5
5
|
const http = require('node:http')
|
|
6
6
|
const util = require('../core/util.js')
|
|
7
|
+
const { ClientStats } = require('../util/stats.js')
|
|
7
8
|
const { channels } = require('../core/diagnostics.js')
|
|
8
9
|
const Request = require('../core/request.js')
|
|
9
10
|
const DispatcherBase = require('./dispatcher-base')
|
|
@@ -260,6 +261,10 @@ class Client extends DispatcherBase {
|
|
|
260
261
|
this[kResume](true)
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
get stats () {
|
|
265
|
+
return new ClientStats(this)
|
|
266
|
+
}
|
|
267
|
+
|
|
263
268
|
get [kPending] () {
|
|
264
269
|
return this[kQueue].length - this[kPendingIdx]
|
|
265
270
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { PoolStats } = require('../util/stats.js')
|
|
3
4
|
const DispatcherBase = require('./dispatcher-base')
|
|
4
5
|
const FixedQueue = require('./fixed-queue')
|
|
5
6
|
const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols')
|
|
6
|
-
const PoolStats = require('./pool-stats')
|
|
7
7
|
|
|
8
8
|
const kClients = Symbol('clients')
|
|
9
9
|
const kNeedDrain = Symbol('needDrain')
|
|
@@ -16,7 +16,6 @@ const kOnConnectionError = Symbol('onConnectionError')
|
|
|
16
16
|
const kGetDispatcher = Symbol('get dispatcher')
|
|
17
17
|
const kAddClient = Symbol('add client')
|
|
18
18
|
const kRemoveClient = Symbol('remove client')
|
|
19
|
-
const kStats = Symbol('stats')
|
|
20
19
|
|
|
21
20
|
class PoolBase extends DispatcherBase {
|
|
22
21
|
constructor () {
|
|
@@ -67,8 +66,6 @@ class PoolBase extends DispatcherBase {
|
|
|
67
66
|
this[kOnConnectionError] = (origin, targets, err) => {
|
|
68
67
|
pool.emit('connectionError', origin, [pool, ...targets], err)
|
|
69
68
|
}
|
|
70
|
-
|
|
71
|
-
this[kStats] = new PoolStats(this)
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
get [kBusy] () {
|
|
@@ -108,7 +105,7 @@ class PoolBase extends DispatcherBase {
|
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
get stats () {
|
|
111
|
-
return this
|
|
108
|
+
return new PoolStats(this)
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
async [kClose] () {
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -5,7 +5,8 @@ const {
|
|
|
5
5
|
kClients,
|
|
6
6
|
kNeedDrain,
|
|
7
7
|
kAddClient,
|
|
8
|
-
kGetDispatcher
|
|
8
|
+
kGetDispatcher,
|
|
9
|
+
kRemoveClient
|
|
9
10
|
} = require('./pool-base')
|
|
10
11
|
const Client = require('./client')
|
|
11
12
|
const {
|
|
@@ -35,6 +36,7 @@ class Pool extends PoolBase {
|
|
|
35
36
|
autoSelectFamily,
|
|
36
37
|
autoSelectFamilyAttemptTimeout,
|
|
37
38
|
allowH2,
|
|
39
|
+
clientTtl,
|
|
38
40
|
...options
|
|
39
41
|
} = {}) {
|
|
40
42
|
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
|
|
@@ -65,12 +67,20 @@ class Pool extends PoolBase {
|
|
|
65
67
|
|
|
66
68
|
this[kConnections] = connections || null
|
|
67
69
|
this[kUrl] = util.parseOrigin(origin)
|
|
68
|
-
this[kOptions] = { ...util.deepClone(options), connect, allowH2 }
|
|
70
|
+
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
|
|
69
71
|
this[kOptions].interceptors = options.interceptors
|
|
70
72
|
? { ...options.interceptors }
|
|
71
73
|
: undefined
|
|
72
74
|
this[kFactory] = factory
|
|
73
75
|
|
|
76
|
+
this.on('connect', (origin, targets) => {
|
|
77
|
+
if (clientTtl != null && clientTtl > 0) {
|
|
78
|
+
for (const target of targets) {
|
|
79
|
+
Object.assign(target, { ttl: Date.now() })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
74
84
|
this.on('connectionError', (origin, targets, error) => {
|
|
75
85
|
// If a connection error occurs, we remove the client from the pool,
|
|
76
86
|
// and emit a connectionError event. They will not be re-used.
|
|
@@ -87,8 +97,12 @@ class Pool extends PoolBase {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
[kGetDispatcher] () {
|
|
100
|
+
const clientTtlOption = this[kOptions].clientTtl
|
|
90
101
|
for (const client of this[kClients]) {
|
|
91
|
-
|
|
102
|
+
// check ttl of client and if it's stale, remove it from the pool
|
|
103
|
+
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
|
|
104
|
+
this[kRemoveClient](client)
|
|
105
|
+
} else if (!client[kNeedDrain]) {
|
|
92
106
|
return client
|
|
93
107
|
}
|
|
94
108
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { kProxy, kClose, kDestroy } = require('../core/symbols')
|
|
3
|
+
const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
|
|
4
4
|
const { URL } = require('node:url')
|
|
5
5
|
const Agent = require('./agent')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const DispatcherBase = require('./dispatcher-base')
|
|
8
8
|
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
|
|
9
9
|
const buildConnector = require('../core/connect')
|
|
10
|
+
const Client = require('./client')
|
|
10
11
|
|
|
11
12
|
const kAgent = Symbol('proxy agent')
|
|
12
13
|
const kClient = Symbol('proxy client')
|
|
@@ -14,6 +15,7 @@ const kProxyHeaders = Symbol('proxy headers')
|
|
|
14
15
|
const kRequestTls = Symbol('request tls settings')
|
|
15
16
|
const kProxyTls = Symbol('proxy tls settings')
|
|
16
17
|
const kConnectEndpoint = Symbol('connect endpoint function')
|
|
18
|
+
const kTunnelProxy = Symbol('tunnel proxy')
|
|
17
19
|
|
|
18
20
|
function defaultProtocolPort (protocol) {
|
|
19
21
|
return protocol === 'https:' ? 443 : 80
|
|
@@ -25,6 +27,61 @@ function defaultFactory (origin, opts) {
|
|
|
25
27
|
|
|
26
28
|
const noop = () => {}
|
|
27
29
|
|
|
30
|
+
class ProxyClient extends DispatcherBase {
|
|
31
|
+
#client = null
|
|
32
|
+
constructor (origin, opts) {
|
|
33
|
+
if (typeof origin === 'string') {
|
|
34
|
+
origin = new URL(origin)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
|
|
38
|
+
throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
super()
|
|
42
|
+
|
|
43
|
+
this.#client = new Client(origin, opts)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async [kClose] () {
|
|
47
|
+
await this.#client.close()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async [kDestroy] () {
|
|
51
|
+
await this.#client.destroy()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async [kDispatch] (opts, handler) {
|
|
55
|
+
const { method, origin } = opts
|
|
56
|
+
if (method === 'CONNECT') {
|
|
57
|
+
this.#client[kConnector]({
|
|
58
|
+
origin,
|
|
59
|
+
port: opts.port || defaultProtocolPort(opts.protocol),
|
|
60
|
+
path: opts.host,
|
|
61
|
+
signal: opts.signal,
|
|
62
|
+
headers: {
|
|
63
|
+
...this[kProxyHeaders],
|
|
64
|
+
host: opts.host
|
|
65
|
+
},
|
|
66
|
+
servername: this[kProxyTls]?.servername || opts.servername
|
|
67
|
+
},
|
|
68
|
+
(err, socket) => {
|
|
69
|
+
if (err) {
|
|
70
|
+
handler.callback(err)
|
|
71
|
+
} else {
|
|
72
|
+
handler.callback(null, { socket, statusCode: 200 })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (typeof origin === 'string') {
|
|
79
|
+
opts.origin = new URL(origin)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.#client.dispatch(opts, handler)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
28
85
|
class ProxyAgent extends DispatcherBase {
|
|
29
86
|
constructor (opts) {
|
|
30
87
|
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
|
|
@@ -36,6 +93,8 @@ class ProxyAgent extends DispatcherBase {
|
|
|
36
93
|
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
|
|
37
94
|
}
|
|
38
95
|
|
|
96
|
+
const { proxyTunnel = true } = opts
|
|
97
|
+
|
|
39
98
|
super()
|
|
40
99
|
|
|
41
100
|
const url = this.#getUrl(opts)
|
|
@@ -57,9 +116,19 @@ class ProxyAgent extends DispatcherBase {
|
|
|
57
116
|
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
|
|
58
117
|
}
|
|
59
118
|
|
|
119
|
+
const factory = (!proxyTunnel && protocol === 'http:')
|
|
120
|
+
? (origin, options) => {
|
|
121
|
+
if (origin.protocol === 'http:') {
|
|
122
|
+
return new ProxyClient(origin, options)
|
|
123
|
+
}
|
|
124
|
+
return new Client(origin, options)
|
|
125
|
+
}
|
|
126
|
+
: undefined
|
|
127
|
+
|
|
60
128
|
const connect = buildConnector({ ...opts.proxyTls })
|
|
61
129
|
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
|
|
62
|
-
this[kClient] = clientFactory(url, { connect })
|
|
130
|
+
this[kClient] = clientFactory(url, { connect, factory })
|
|
131
|
+
this[kTunnelProxy] = proxyTunnel
|
|
63
132
|
this[kAgent] = new Agent({
|
|
64
133
|
...opts,
|
|
65
134
|
connect: async (opts, callback) => {
|
|
@@ -115,6 +184,10 @@ class ProxyAgent extends DispatcherBase {
|
|
|
115
184
|
headers.host = host
|
|
116
185
|
}
|
|
117
186
|
|
|
187
|
+
if (!this.#shouldConnect(new URL(opts.origin))) {
|
|
188
|
+
opts.path = opts.origin + opts.path
|
|
189
|
+
}
|
|
190
|
+
|
|
118
191
|
return this[kAgent].dispatch(
|
|
119
192
|
{
|
|
120
193
|
...opts,
|
|
@@ -147,6 +220,19 @@ class ProxyAgent extends DispatcherBase {
|
|
|
147
220
|
await this[kAgent].destroy()
|
|
148
221
|
await this[kClient].destroy()
|
|
149
222
|
}
|
|
223
|
+
|
|
224
|
+
#shouldConnect (uri) {
|
|
225
|
+
if (typeof uri === 'string') {
|
|
226
|
+
uri = new URL(uri)
|
|
227
|
+
}
|
|
228
|
+
if (this[kTunnelProxy]) {
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
150
236
|
}
|
|
151
237
|
|
|
152
238
|
/**
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -20,7 +20,12 @@ const { AbortError } = require('../core/errors.js')
|
|
|
20
20
|
*/
|
|
21
21
|
function needsRevalidation (result, cacheControlDirectives) {
|
|
22
22
|
if (cacheControlDirectives?.['no-cache']) {
|
|
23
|
-
// Always revalidate requests with the no-cache directive
|
|
23
|
+
// Always revalidate requests with the no-cache request directive
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) {
|
|
28
|
+
// Always revalidate requests with unqualified no-cache response directive
|
|
24
29
|
return true
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -233,7 +238,7 @@ function handleResult (
|
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
let headers = {
|
|
236
|
-
...
|
|
241
|
+
...opts.headers,
|
|
237
242
|
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
238
243
|
}
|
|
239
244
|
|
|
@@ -319,6 +324,11 @@ module.exports = (opts = {}) => {
|
|
|
319
324
|
return dispatch(opts, handler)
|
|
320
325
|
}
|
|
321
326
|
|
|
327
|
+
opts = {
|
|
328
|
+
...opts,
|
|
329
|
+
headers: normaliseHeaders(opts)
|
|
330
|
+
}
|
|
331
|
+
|
|
322
332
|
const reqCacheControl = opts.headers?.['cache-control']
|
|
323
333
|
? parseCacheControlHeader(opts.headers['cache-control'])
|
|
324
334
|
: undefined
|
package/lib/mock/mock-agent.js
CHANGED
|
@@ -16,11 +16,12 @@ const {
|
|
|
16
16
|
kMockAgentIsCallHistoryEnabled,
|
|
17
17
|
kMockAgentAddCallHistoryLog,
|
|
18
18
|
kMockAgentMockCallHistoryInstance,
|
|
19
|
+
kMockAgentAcceptsNonStandardSearchParameters,
|
|
19
20
|
kMockCallHistoryAddLog
|
|
20
21
|
} = require('./mock-symbols')
|
|
21
22
|
const MockClient = require('./mock-client')
|
|
22
23
|
const MockPool = require('./mock-pool')
|
|
23
|
-
const { matchValue, buildAndValidateMockOptions } = require('./mock-utils')
|
|
24
|
+
const { matchValue, normalizeSearchParams, buildAndValidateMockOptions } = require('./mock-utils')
|
|
24
25
|
const { InvalidArgumentError, UndiciError } = require('../core/errors')
|
|
25
26
|
const Dispatcher = require('../dispatcher/dispatcher')
|
|
26
27
|
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
|
|
@@ -35,6 +36,7 @@ class MockAgent extends Dispatcher {
|
|
|
35
36
|
this[kNetConnect] = true
|
|
36
37
|
this[kIsMockActive] = true
|
|
37
38
|
this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
|
|
39
|
+
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions?.acceptNonStandardSearchParameters ?? false
|
|
38
40
|
|
|
39
41
|
// Instantiate Agent and encapsulate
|
|
40
42
|
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
|
|
@@ -67,7 +69,17 @@ class MockAgent extends Dispatcher {
|
|
|
67
69
|
|
|
68
70
|
this[kMockAgentAddCallHistoryLog](opts)
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters]
|
|
73
|
+
|
|
74
|
+
const dispatchOpts = { ...opts }
|
|
75
|
+
|
|
76
|
+
if (acceptNonStandardSearchParameters && dispatchOpts.path) {
|
|
77
|
+
const [path, searchParams] = dispatchOpts.path.split('?')
|
|
78
|
+
const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters)
|
|
79
|
+
dispatchOpts.path = `${path}?${normalizedSearchParams}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this[kAgent].dispatch(dispatchOpts, handler)
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
async close () {
|
|
@@ -147,7 +159,7 @@ class MockAgent extends Dispatcher {
|
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
[kMockAgentSet] (origin, dispatcher) {
|
|
150
|
-
this[kClients].set(origin, dispatcher)
|
|
162
|
+
this[kClients].set(origin, { count: 0, dispatcher })
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
[kFactory] (origin) {
|
|
@@ -159,9 +171,9 @@ class MockAgent extends Dispatcher {
|
|
|
159
171
|
|
|
160
172
|
[kMockAgentGet] (origin) {
|
|
161
173
|
// First check if we can immediately find it
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
return
|
|
174
|
+
const result = this[kClients].get(origin)
|
|
175
|
+
if (result?.dispatcher) {
|
|
176
|
+
return result.dispatcher
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
// If the origin is not a string create a dummy parent pool and return to user
|
|
@@ -172,11 +184,11 @@ class MockAgent extends Dispatcher {
|
|
|
172
184
|
}
|
|
173
185
|
|
|
174
186
|
// If we match, create a pool and assign the same dispatches
|
|
175
|
-
for (const [keyMatcher,
|
|
176
|
-
if (
|
|
187
|
+
for (const [keyMatcher, result] of Array.from(this[kClients])) {
|
|
188
|
+
if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
|
|
177
189
|
const dispatcher = this[kFactory](origin)
|
|
178
190
|
this[kMockAgentSet](origin, dispatcher)
|
|
179
|
-
dispatcher[kDispatches] =
|
|
191
|
+
dispatcher[kDispatches] = result.dispatcher[kDispatches]
|
|
180
192
|
return dispatcher
|
|
181
193
|
}
|
|
182
194
|
}
|
|
@@ -190,7 +202,7 @@ class MockAgent extends Dispatcher {
|
|
|
190
202
|
const mockAgentClients = this[kClients]
|
|
191
203
|
|
|
192
204
|
return Array.from(mockAgentClients.entries())
|
|
193
|
-
.flatMap(([origin,
|
|
205
|
+
.flatMap(([origin, result]) => result.dispatcher[kDispatches].map(dispatch => ({ ...dispatch, origin })))
|
|
194
206
|
.filter(({ pending }) => pending)
|
|
195
207
|
}
|
|
196
208
|
|
package/lib/mock/mock-symbols.js
CHANGED
|
@@ -26,5 +26,6 @@ module.exports = {
|
|
|
26
26
|
kMockAgentRegisterCallHistory: Symbol('mock agent register mock call history'),
|
|
27
27
|
kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'),
|
|
28
28
|
kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'),
|
|
29
|
+
kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'),
|
|
29
30
|
kMockCallHistoryAddLog: Symbol('mock call history add log')
|
|
30
31
|
}
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -92,13 +92,42 @@ function matchHeaders (mockDispatch, headers) {
|
|
|
92
92
|
return true
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function normalizeSearchParams (query) {
|
|
96
|
+
if (typeof query !== 'string') {
|
|
97
|
+
return query
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const originalQp = new URLSearchParams(query)
|
|
101
|
+
const normalizedQp = new URLSearchParams()
|
|
102
|
+
|
|
103
|
+
for (let [key, value] of originalQp.entries()) {
|
|
104
|
+
key = key.replace('[]', '')
|
|
105
|
+
|
|
106
|
+
const valueRepresentsString = /^(['"]).*\1$/.test(value)
|
|
107
|
+
if (valueRepresentsString) {
|
|
108
|
+
normalizedQp.append(key, value)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (value.includes(',')) {
|
|
113
|
+
const values = value.split(',')
|
|
114
|
+
for (const v of values) {
|
|
115
|
+
normalizedQp.append(key, v)
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
normalizedQp.append(key, value)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalizedQp
|
|
124
|
+
}
|
|
125
|
+
|
|
95
126
|
function safeUrl (path) {
|
|
96
127
|
if (typeof path !== 'string') {
|
|
97
128
|
return path
|
|
98
129
|
}
|
|
99
|
-
|
|
100
130
|
const pathSegments = path.split('?', 3)
|
|
101
|
-
|
|
102
131
|
if (pathSegments.length !== 2) {
|
|
103
132
|
return path
|
|
104
133
|
}
|
|
@@ -376,6 +405,10 @@ function buildAndValidateMockOptions (opts) {
|
|
|
376
405
|
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
|
|
377
406
|
}
|
|
378
407
|
|
|
408
|
+
if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') {
|
|
409
|
+
throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean')
|
|
410
|
+
}
|
|
411
|
+
|
|
379
412
|
return mockOptions
|
|
380
413
|
}
|
|
381
414
|
}
|
|
@@ -395,5 +428,6 @@ module.exports = {
|
|
|
395
428
|
checkNetConnect,
|
|
396
429
|
buildAndValidateMockOptions,
|
|
397
430
|
getHeaderByName,
|
|
398
|
-
buildHeadersFromArray
|
|
431
|
+
buildHeadersFromArray,
|
|
432
|
+
normalizeSearchParams
|
|
399
433
|
}
|
package/lib/util/cache.js
CHANGED
|
@@ -12,13 +12,11 @@ function makeCacheKey (opts) {
|
|
|
12
12
|
throw new Error('opts.origin is undefined')
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const headers = normaliseHeaders(opts)
|
|
16
|
-
|
|
17
15
|
return {
|
|
18
16
|
origin: opts.origin.toString(),
|
|
19
17
|
method: opts.method,
|
|
20
18
|
path: opts.path,
|
|
21
|
-
headers
|
|
19
|
+
headers: opts.headers
|
|
22
20
|
}
|
|
23
21
|
}
|
|
24
22
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
kConnected,
|
|
5
|
+
kPending,
|
|
6
|
+
kRunning,
|
|
7
|
+
kSize,
|
|
8
|
+
kFree,
|
|
9
|
+
kQueued
|
|
10
|
+
} = require('../core/symbols')
|
|
11
|
+
|
|
12
|
+
class ClientStats {
|
|
13
|
+
constructor (client) {
|
|
14
|
+
this.connected = client[kConnected]
|
|
15
|
+
this.pending = client[kPending]
|
|
16
|
+
this.running = client[kRunning]
|
|
17
|
+
this.size = client[kSize]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class PoolStats {
|
|
22
|
+
constructor (pool) {
|
|
23
|
+
this.connected = pool[kConnected]
|
|
24
|
+
this.free = pool[kFree]
|
|
25
|
+
this.pending = pool[kPending]
|
|
26
|
+
this.queued = pool[kQueued]
|
|
27
|
+
this.running = pool[kRunning]
|
|
28
|
+
this.size = pool[kSize]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { ClientStats, PoolStats }
|
package/package.json
CHANGED
package/types/agent.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { URL } from 'url'
|
|
2
2
|
import Pool from './pool'
|
|
3
3
|
import Dispatcher from './dispatcher'
|
|
4
|
+
import TClientStats from './client-stats'
|
|
5
|
+
import TPoolStats from './pool-stats'
|
|
4
6
|
|
|
5
7
|
export default Agent
|
|
6
8
|
|
|
@@ -12,6 +14,8 @@ declare class Agent extends Dispatcher {
|
|
|
12
14
|
destroyed: boolean
|
|
13
15
|
/** Dispatches a request. */
|
|
14
16
|
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean
|
|
17
|
+
/** Aggregate stats for a Agent by origin. */
|
|
18
|
+
readonly stats: Record<string, TClientStats | TPoolStats>
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
declare namespace Agent {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Client from './client'
|
|
2
|
+
|
|
3
|
+
export default ClientStats
|
|
4
|
+
|
|
5
|
+
declare class ClientStats {
|
|
6
|
+
constructor (pool: Client)
|
|
7
|
+
/** If socket has open connection. */
|
|
8
|
+
connected: boolean
|
|
9
|
+
/** Number of open socket connections in this client that do not have an active request. */
|
|
10
|
+
pending: number
|
|
11
|
+
/** Number of currently active requests of this client. */
|
|
12
|
+
running: number
|
|
13
|
+
/** Number of active, pending, or queued requests of this client. */
|
|
14
|
+
size: number
|
|
15
|
+
}
|
package/types/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { URL } from 'url'
|
|
2
2
|
import Dispatcher from './dispatcher'
|
|
3
3
|
import buildConnector from './connector'
|
|
4
|
+
import TClientStats from './client-stats'
|
|
4
5
|
|
|
5
6
|
type ClientConnectOptions = Omit<Dispatcher.ConnectOptions, 'origin'>
|
|
6
7
|
|
|
@@ -15,6 +16,8 @@ export class Client extends Dispatcher {
|
|
|
15
16
|
closed: boolean
|
|
16
17
|
/** `true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed. */
|
|
17
18
|
destroyed: boolean
|
|
19
|
+
/** Aggregate stats for a Client. */
|
|
20
|
+
readonly stats: TClientStats
|
|
18
21
|
|
|
19
22
|
// Override dispatcher APIs.
|
|
20
23
|
override connect (
|
|
@@ -84,13 +87,13 @@ export declare namespace Client {
|
|
|
84
87
|
/**
|
|
85
88
|
* @description Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
|
|
86
89
|
* @default false
|
|
87
|
-
|
|
90
|
+
*/
|
|
88
91
|
allowH2?: boolean;
|
|
89
92
|
/**
|
|
90
93
|
* @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
|
|
91
94
|
* @default 100
|
|
92
|
-
|
|
93
|
-
maxConcurrentStreams?: number
|
|
95
|
+
*/
|
|
96
|
+
maxConcurrentStreams?: number;
|
|
94
97
|
}
|
|
95
98
|
export interface SocketInfo {
|
|
96
99
|
localAddress?: string
|
package/types/mock-agent.d.ts
CHANGED
|
@@ -59,6 +59,9 @@ declare namespace MockAgent {
|
|
|
59
59
|
/** Ignore trailing slashes in the path */
|
|
60
60
|
ignoreTrailingSlash?: boolean;
|
|
61
61
|
|
|
62
|
+
/** Accept URLs with search parameters using non standard syntaxes. default false */
|
|
63
|
+
acceptNonStandardSearchParameters?: boolean;
|
|
64
|
+
|
|
62
65
|
/** Enable call history. you can either call MockAgent.enableCallHistory(). default false */
|
|
63
66
|
enableCallHistory?: boolean
|
|
64
67
|
}
|
package/types/pool.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ declare namespace Pool {
|
|
|
33
33
|
factory?(origin: URL, opts: object): Dispatcher;
|
|
34
34
|
/** The max number of clients to create. `null` if no limit. Default `null`. */
|
|
35
35
|
connections?: number | null;
|
|
36
|
+
/** The amount of time before a client is removed from the pool and closed. `null` if no time limit. Default `null` */
|
|
37
|
+
clientTtl?: number | null;
|
|
36
38
|
|
|
37
39
|
interceptors?: { Pool?: readonly Dispatcher.DispatchInterceptor[] } & Client.Options['interceptors']
|
|
38
40
|
}
|
package/types/proxy-agent.d.ts
CHANGED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require('../core/symbols')
|
|
4
|
-
const kPool = Symbol('pool')
|
|
5
|
-
|
|
6
|
-
class PoolStats {
|
|
7
|
-
constructor (pool) {
|
|
8
|
-
this[kPool] = pool
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
get connected () {
|
|
12
|
-
return this[kPool][kConnected]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
get free () {
|
|
16
|
-
return this[kPool][kFree]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
get pending () {
|
|
20
|
-
return this[kPool][kPending]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
get queued () {
|
|
24
|
-
return this[kPool][kQueued]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
get running () {
|
|
28
|
-
return this[kPool][kRunning]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get size () {
|
|
32
|
-
return this[kPool][kSize]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.exports = PoolStats
|