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 +2 -1
- package/docs/docs/api/Dispatcher.md +1 -1
- package/docs/docs/api/EnvHttpProxyAgent.md +0 -2
- package/docs/docs/api/Errors.md +1 -0
- package/docs/docs/api/MockAgent.md +59 -0
- package/docs/docs/api/MockCallHistory.md +197 -0
- package/docs/docs/api/MockCallHistoryLog.md +43 -0
- package/docs/docs/best-practices/mocking-request.md +55 -1
- package/index-fetch.js +3 -0
- package/index.js +3 -0
- package/lib/cache/memory-cache-store.js +7 -1
- package/lib/cache/sqlite-cache-store.js +11 -12
- package/lib/dispatcher/client.js +1 -1
- package/lib/dispatcher/env-http-proxy-agent.js +0 -9
- package/lib/dispatcher/pool.js +15 -1
- package/lib/handler/retry-handler.js +3 -3
- package/lib/mock/mock-agent.js +59 -4
- package/lib/mock/mock-call-history.js +248 -0
- package/lib/mock/mock-symbols.js +6 -1
- package/lib/mock/mock-utils.js +11 -3
- package/lib/util/cache.js +9 -8
- package/lib/web/fetch/body.js +1 -1
- package/lib/web/fetch/request.js +12 -9
- package/package.json +1 -1
- package/types/cache-interceptor.d.ts +2 -2
- package/types/client.d.ts +1 -1
- package/types/index.d.ts +4 -1
- package/types/mock-agent.d.ts +12 -0
- package/types/mock-call-history.d.ts +111 -0
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.
|
package/docs/docs/api/Errors.md
CHANGED
|
@@ -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 =>
|
|
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
|
-
|
|
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
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -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)
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -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
|
|
15
|
-
return
|
|
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(
|
|
127
|
+
? calculateRetryAfterHeader(headers['retry-after'])
|
|
128
128
|
: retryAfterHeader * 1e3 // Retry-After is in seconds
|
|
129
129
|
}
|
|
130
130
|
|