undici 7.8.0 → 7.9.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/ClientStats.md +27 -0
- package/docs/docs/api/DiagnosticsChannel.md +1 -1
- package/docs/docs/api/MockAgent.md +2 -0
- package/lib/cache/memory-cache-store.js +25 -12
- package/lib/dispatcher/agent.js +11 -1
- 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/interceptor/cache.js +12 -2
- package/lib/mock/mock-agent.js +14 -2
- 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/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).
|
|
@@ -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.
|
|
@@ -76,17 +76,9 @@ class MemoryCacheStore {
|
|
|
76
76
|
const topLevelKey = `${key.origin}:${key.path}`
|
|
77
77
|
|
|
78
78
|
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
|
-
}
|
|
79
|
+
const entries = this.#entries.get(topLevelKey)
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
}))
|
|
89
|
-
))
|
|
81
|
+
const entry = entries ? findEntry(key, entries, now) : null
|
|
90
82
|
|
|
91
83
|
return entry == null
|
|
92
84
|
? undefined
|
|
@@ -140,10 +132,17 @@ class MemoryCacheStore {
|
|
|
140
132
|
entries = []
|
|
141
133
|
store.#entries.set(topLevelKey, entries)
|
|
142
134
|
}
|
|
143
|
-
entries.
|
|
135
|
+
const previousEntry = findEntry(key, entries, Date.now())
|
|
136
|
+
if (previousEntry) {
|
|
137
|
+
const index = entries.indexOf(previousEntry)
|
|
138
|
+
entries.splice(index, 1, entry)
|
|
139
|
+
store.#size -= previousEntry.size
|
|
140
|
+
} else {
|
|
141
|
+
entries.push(entry)
|
|
142
|
+
store.#count += 1
|
|
143
|
+
}
|
|
144
144
|
|
|
145
145
|
store.#size += entry.size
|
|
146
|
-
store.#count += 1
|
|
147
146
|
|
|
148
147
|
if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
|
|
149
148
|
for (const [key, entries] of store.#entries) {
|
|
@@ -180,4 +179,18 @@ class MemoryCacheStore {
|
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
|
|
182
|
+
function findEntry (key, entries, now) {
|
|
183
|
+
return entries.find((entry) => (
|
|
184
|
+
entry.deleteAt > now &&
|
|
185
|
+
entry.method === key.method &&
|
|
186
|
+
(entry.vary == null || Object.keys(entry.vary).every(headerName => {
|
|
187
|
+
if (entry.vary[headerName] === null) {
|
|
188
|
+
return key.headers[headerName] === undefined
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return entry.vary[headerName] === key.headers[headerName]
|
|
192
|
+
}))
|
|
193
|
+
))
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
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')
|
|
@@ -110,6 +110,16 @@ class Agent extends DispatcherBase {
|
|
|
110
110
|
|
|
111
111
|
await Promise.all(destroyPromises)
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
get stats () {
|
|
115
|
+
const allClientStats = {}
|
|
116
|
+
for (const client of this[kClients].values()) {
|
|
117
|
+
if (client.stats) {
|
|
118
|
+
allClientStats[client[kUrl].origin] = client.stats
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return allClientStats
|
|
122
|
+
}
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
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/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 () {
|
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
|
}
|
|
@@ -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
|