undici 5.8.0 → 5.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/docs/api/Client.md +1 -1
- package/docs/api/Dispatcher.md +1 -1
- package/docs/api/MockAgent.md +17 -0
- package/docs/best-practices/mocking-request.md +9 -9
- package/index.js +6 -1
- package/lib/client.js +15 -2
- package/lib/core/connect.js +28 -5
- package/lib/core/request.js +2 -1
- package/lib/core/util.js +19 -4
- package/lib/fetch/body.js +12 -1
- package/lib/fetch/dataURL.js +1 -1
- package/lib/fetch/file.js +3 -1
- package/lib/fetch/index.js +14 -17
- package/lib/fetch/request.js +5 -5
- package/lib/fetch/response.js +8 -7
- package/lib/fetch/util.js +61 -2
- package/lib/fetch/webidl.js +9 -5
- package/lib/mock/mock-utils.js +20 -9
- package/package.json +2 -1
- package/types/mock-interceptor.d.ts +1 -1
- package/types/pool-stats.d.ts +19 -0
- package/types/pool.d.ts +4 -0
package/docs/api/Client.md
CHANGED
|
@@ -18,7 +18,7 @@ Returns: `Client`
|
|
|
18
18
|
### Parameter: `ClientOptions`
|
|
19
19
|
|
|
20
20
|
* **bodyTimeout** `number | null` (optional) - Default: `30e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
|
|
21
|
-
* **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
|
|
21
|
+
* **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
|
|
22
22
|
* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
|
|
23
23
|
* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
|
|
24
24
|
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second.
|
package/docs/api/Dispatcher.md
CHANGED
|
@@ -199,7 +199,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
|
|
|
199
199
|
* **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
|
|
200
200
|
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
|
|
201
201
|
* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
|
|
202
|
-
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
|
|
202
|
+
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
|
|
203
203
|
* **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
|
|
204
204
|
|
|
205
205
|
#### Parameter: `DispatchHandler`
|
package/docs/api/MockAgent.md
CHANGED
|
@@ -177,6 +177,23 @@ for await (const data of result2.body) {
|
|
|
177
177
|
console.log('data', data.toString('utf8')) // data hello
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
|
+
#### Example - Mock different requests within the same file
|
|
181
|
+
```js
|
|
182
|
+
const { MockAgent, setGlobalDispatcher } = require('undici');
|
|
183
|
+
const agent = new MockAgent();
|
|
184
|
+
agent.disableNetConnect();
|
|
185
|
+
setGlobalDispatcher(agent);
|
|
186
|
+
describe('Test', () => {
|
|
187
|
+
it('200', async () => {
|
|
188
|
+
const mockAgent = agent.get('http://test.com');
|
|
189
|
+
// your test
|
|
190
|
+
});
|
|
191
|
+
it('200', async () => {
|
|
192
|
+
const mockAgent = agent.get('http://testing.com');
|
|
193
|
+
// your test
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
```
|
|
180
197
|
|
|
181
198
|
#### Example - Mocked request with query body, headers and trailers
|
|
182
199
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mocking Request
|
|
2
2
|
|
|
3
|
-
Undici
|
|
3
|
+
Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
|
|
4
4
|
|
|
5
5
|
Example:
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Example:
|
|
|
8
8
|
// bank.mjs
|
|
9
9
|
import { request } from 'undici'
|
|
10
10
|
|
|
11
|
-
export async function bankTransfer(
|
|
11
|
+
export async function bankTransfer(recipient, amount) {
|
|
12
12
|
const { body } = await request('http://localhost:3000/bank-transfer',
|
|
13
13
|
{
|
|
14
14
|
method: 'POST',
|
|
@@ -16,7 +16,7 @@ export async function bankTransfer(recepient, amount) {
|
|
|
16
16
|
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
17
17
|
},
|
|
18
18
|
body: JSON.stringify({
|
|
19
|
-
|
|
19
|
+
recipient,
|
|
20
20
|
amount
|
|
21
21
|
})
|
|
22
22
|
}
|
|
@@ -48,7 +48,7 @@ mockPool.intercept({
|
|
|
48
48
|
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
49
49
|
},
|
|
50
50
|
body: JSON.stringify({
|
|
51
|
-
|
|
51
|
+
recipient: '1234567890',
|
|
52
52
|
amount: '100'
|
|
53
53
|
})
|
|
54
54
|
}).reply(200, {
|
|
@@ -77,7 +77,7 @@ Explore other MockAgent functionality [here](../api/MockAgent.md)
|
|
|
77
77
|
|
|
78
78
|
## Debug Mock Value
|
|
79
79
|
|
|
80
|
-
When the interceptor
|
|
80
|
+
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()`:
|
|
81
81
|
|
|
82
82
|
```js
|
|
83
83
|
const mockAgent = new MockAgent();
|
|
@@ -89,7 +89,7 @@ mockAgent.disableNetConnect()
|
|
|
89
89
|
const mockPool = mockAgent.get('http://localhost:3000');
|
|
90
90
|
|
|
91
91
|
mockPool.intercept({
|
|
92
|
-
path: '/bank-
|
|
92
|
+
path: '/bank-transfer',
|
|
93
93
|
method: 'POST',
|
|
94
94
|
}).reply(200, {
|
|
95
95
|
message: 'transaction processed'
|
|
@@ -103,7 +103,7 @@ const badRequest = await bankTransfer('1234567890', '100')
|
|
|
103
103
|
|
|
104
104
|
## Reply with data based on request
|
|
105
105
|
|
|
106
|
-
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply
|
|
106
|
+
If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`:
|
|
107
107
|
|
|
108
108
|
```js
|
|
109
109
|
mockPool.intercept({
|
|
@@ -113,7 +113,7 @@ mockPool.intercept({
|
|
|
113
113
|
'X-TOKEN-SECRET': 'SuperSecretToken',
|
|
114
114
|
},
|
|
115
115
|
body: JSON.stringify({
|
|
116
|
-
|
|
116
|
+
recipient: '1234567890',
|
|
117
117
|
amount: '100'
|
|
118
118
|
})
|
|
119
119
|
}).reply(200, (opts) => {
|
|
@@ -129,7 +129,7 @@ in this case opts will be
|
|
|
129
129
|
{
|
|
130
130
|
method: 'POST',
|
|
131
131
|
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
|
|
132
|
-
body: '{"
|
|
132
|
+
body: '{"recipient":"1234567890","amount":"100"}',
|
|
133
133
|
origin: 'http://localhost:3000',
|
|
134
134
|
path: '/bank-transfer'
|
|
135
135
|
}
|
package/index.js
CHANGED
|
@@ -53,7 +53,12 @@ function makeDispatcher (fn) {
|
|
|
53
53
|
throw new InvalidArgumentError('invalid opts.path')
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
let path = opts.path
|
|
57
|
+
if (!opts.path.startsWith('/')) {
|
|
58
|
+
path = `/${path}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
url = new URL(util.parseOrigin(url).origin + path)
|
|
57
62
|
} else {
|
|
58
63
|
if (!opts) {
|
|
59
64
|
opts = typeof url === 'object' ? url : {}
|
package/lib/client.js
CHANGED
|
@@ -889,8 +889,10 @@ function onParserTimeout (parser) {
|
|
|
889
889
|
|
|
890
890
|
/* istanbul ignore else */
|
|
891
891
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
892
|
-
|
|
893
|
-
|
|
892
|
+
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
|
893
|
+
assert(!parser.paused, 'cannot be paused while waiting for headers')
|
|
894
|
+
util.destroy(socket, new HeadersTimeoutError())
|
|
895
|
+
}
|
|
894
896
|
} else if (timeoutType === TIMEOUT_BODY) {
|
|
895
897
|
if (!parser.paused) {
|
|
896
898
|
util.destroy(socket, new BodyTimeoutError())
|
|
@@ -1641,7 +1643,18 @@ class AsyncWriter {
|
|
|
1641
1643
|
this.bytesWritten += len
|
|
1642
1644
|
|
|
1643
1645
|
const ret = socket.write(chunk)
|
|
1646
|
+
|
|
1644
1647
|
request.onBodySent(chunk)
|
|
1648
|
+
|
|
1649
|
+
if (!ret) {
|
|
1650
|
+
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1651
|
+
// istanbul ignore else: only for jest
|
|
1652
|
+
if (socket[kParser].timeout.refresh) {
|
|
1653
|
+
socket[kParser].timeout.refresh()
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1645
1658
|
return ret
|
|
1646
1659
|
}
|
|
1647
1660
|
|
package/lib/core/connect.js
CHANGED
|
@@ -75,14 +75,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
75
75
|
})
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const
|
|
79
|
-
? setTimeout(onConnectTimeout, timeout, socket)
|
|
80
|
-
: null
|
|
78
|
+
const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
|
|
81
79
|
|
|
82
80
|
socket
|
|
83
81
|
.setNoDelay(true)
|
|
84
82
|
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
|
|
85
|
-
|
|
83
|
+
cancelTimeout()
|
|
86
84
|
|
|
87
85
|
if (callback) {
|
|
88
86
|
const cb = callback
|
|
@@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
91
89
|
}
|
|
92
90
|
})
|
|
93
91
|
.on('error', function (err) {
|
|
94
|
-
|
|
92
|
+
cancelTimeout()
|
|
95
93
|
|
|
96
94
|
if (callback) {
|
|
97
95
|
const cb = callback
|
|
@@ -104,6 +102,31 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
|
|
|
104
102
|
}
|
|
105
103
|
}
|
|
106
104
|
|
|
105
|
+
function setupTimeout (onConnectTimeout, timeout) {
|
|
106
|
+
if (!timeout) {
|
|
107
|
+
return () => {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let s1 = null
|
|
111
|
+
let s2 = null
|
|
112
|
+
const timeoutId = setTimeout(() => {
|
|
113
|
+
// setImmediate is added to make sure that we priotorise socket error events over timeouts
|
|
114
|
+
s1 = setImmediate(() => {
|
|
115
|
+
if (process.platform === 'win32') {
|
|
116
|
+
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
|
|
117
|
+
s2 = setImmediate(() => onConnectTimeout())
|
|
118
|
+
} else {
|
|
119
|
+
onConnectTimeout()
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}, timeout)
|
|
123
|
+
return () => {
|
|
124
|
+
clearTimeout(timeoutId)
|
|
125
|
+
clearImmediate(s1)
|
|
126
|
+
clearImmediate(s2)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
function onConnectTimeout (socket) {
|
|
108
131
|
util.destroy(socket, new ConnectTimeoutError())
|
|
109
132
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -297,7 +297,8 @@ function processHeader (request, key, val) {
|
|
|
297
297
|
} else if (
|
|
298
298
|
request.contentType === null &&
|
|
299
299
|
key.length === 12 &&
|
|
300
|
-
key.toLowerCase() === 'content-type'
|
|
300
|
+
key.toLowerCase() === 'content-type' &&
|
|
301
|
+
headerCharRegex.exec(val) === null
|
|
301
302
|
) {
|
|
302
303
|
request.contentType = val
|
|
303
304
|
request.headers += `${key}: ${val}\r\n`
|
package/lib/core/util.js
CHANGED
|
@@ -108,14 +108,25 @@ function parseURL (url) {
|
|
|
108
108
|
const port = url.port != null
|
|
109
109
|
? url.port
|
|
110
110
|
: (url.protocol === 'https:' ? 443 : 80)
|
|
111
|
-
|
|
111
|
+
let origin = url.origin != null
|
|
112
112
|
? url.origin
|
|
113
113
|
: `${url.protocol}//${url.hostname}:${port}`
|
|
114
|
-
|
|
114
|
+
let path = url.path != null
|
|
115
115
|
? url.path
|
|
116
116
|
: `${url.pathname || ''}${url.search || ''}`
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
if (origin.endsWith('/')) {
|
|
119
|
+
origin = origin.substring(0, origin.length - 1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (path && !path.startsWith('/')) {
|
|
123
|
+
path = `/${path}`
|
|
124
|
+
}
|
|
125
|
+
// new URL(path, origin) is unsafe when `path` contains an absolute URL
|
|
126
|
+
// From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
|
|
127
|
+
// If first parameter is a relative URL, second param is required, and will be used as the base URL.
|
|
128
|
+
// If first parameter is an absolute URL, a given second param will be ignored.
|
|
129
|
+
url = new URL(origin + path)
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
return url
|
|
@@ -233,7 +244,11 @@ function parseHeaders (headers, obj = {}) {
|
|
|
233
244
|
const key = headers[i].toString().toLowerCase()
|
|
234
245
|
let val = obj[key]
|
|
235
246
|
if (!val) {
|
|
236
|
-
|
|
247
|
+
if (Array.isArray(headers[i + 1])) {
|
|
248
|
+
obj[key] = headers[i + 1]
|
|
249
|
+
} else {
|
|
250
|
+
obj[key] = headers[i + 1].toString()
|
|
251
|
+
}
|
|
237
252
|
} else {
|
|
238
253
|
if (!Array.isArray(val)) {
|
|
239
254
|
val = [val]
|
package/lib/fetch/body.js
CHANGED
|
@@ -404,7 +404,18 @@ function bodyMixinMethods (instance) {
|
|
|
404
404
|
// 1. Let entries be the result of parsing bytes.
|
|
405
405
|
let entries
|
|
406
406
|
try {
|
|
407
|
-
|
|
407
|
+
let text = ''
|
|
408
|
+
// application/x-www-form-urlencoded parser will keep the BOM.
|
|
409
|
+
// https://url.spec.whatwg.org/#concept-urlencoded-parser
|
|
410
|
+
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
411
|
+
for await (const chunk of consumeBody(this[kState].body)) {
|
|
412
|
+
if (!isUint8Array(chunk)) {
|
|
413
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
414
|
+
}
|
|
415
|
+
text += textDecoder.decode(chunk, { stream: true })
|
|
416
|
+
}
|
|
417
|
+
text += textDecoder.decode()
|
|
418
|
+
entries = new URLSearchParams(text)
|
|
408
419
|
} catch (err) {
|
|
409
420
|
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
|
|
410
421
|
// 2. If entries is failure, then throw a TypeError.
|
package/lib/fetch/dataURL.js
CHANGED
package/lib/fetch/file.js
CHANGED
|
@@ -278,7 +278,9 @@ function processBlobParts (parts, options) {
|
|
|
278
278
|
if (!element.buffer) { // ArrayBuffer
|
|
279
279
|
bytes.push(new Uint8Array(element))
|
|
280
280
|
} else {
|
|
281
|
-
bytes.push(
|
|
281
|
+
bytes.push(
|
|
282
|
+
new Uint8Array(element.buffer, element.byteOffset, element.byteLength)
|
|
283
|
+
)
|
|
282
284
|
}
|
|
283
285
|
} else if (isBlobLike(element)) {
|
|
284
286
|
// 3. If element is a Blob, append the bytes it represents
|
package/lib/fetch/index.js
CHANGED
|
@@ -33,7 +33,9 @@ const {
|
|
|
33
33
|
isBlobLike,
|
|
34
34
|
sameOrigin,
|
|
35
35
|
isCancelled,
|
|
36
|
-
isAborted
|
|
36
|
+
isAborted,
|
|
37
|
+
isErrorLike,
|
|
38
|
+
fullyReadBody
|
|
37
39
|
} = require('./util')
|
|
38
40
|
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
|
|
39
41
|
const assert = require('assert')
|
|
@@ -737,11 +739,7 @@ async function mainFetch (fetchParams, recursive = false) {
|
|
|
737
739
|
}
|
|
738
740
|
|
|
739
741
|
// 4. Fully read response’s body given processBody and processBodyError.
|
|
740
|
-
|
|
741
|
-
processBody(await response.arrayBuffer())
|
|
742
|
-
} catch (err) {
|
|
743
|
-
processBodyError(err)
|
|
744
|
-
}
|
|
742
|
+
await fullyReadBody(response.body, processBody, processBodyError)
|
|
745
743
|
} else {
|
|
746
744
|
// 21. Otherwise, run fetch finale given fetchParams and response.
|
|
747
745
|
fetchFinale(fetchParams, response)
|
|
@@ -973,11 +971,7 @@ async function fetchFinale (fetchParams, response) {
|
|
|
973
971
|
} else {
|
|
974
972
|
// 4. Otherwise, fully read response’s body given processBody, processBodyError,
|
|
975
973
|
// and fetchParams’s task destination.
|
|
976
|
-
|
|
977
|
-
processBody(await response.body.stream.arrayBuffer())
|
|
978
|
-
} catch (err) {
|
|
979
|
-
processBodyError(err)
|
|
980
|
-
}
|
|
974
|
+
await fullyReadBody(response.body, processBody, processBodyError)
|
|
981
975
|
}
|
|
982
976
|
}
|
|
983
977
|
}
|
|
@@ -1854,7 +1848,7 @@ async function httpNetworkFetch (
|
|
|
1854
1848
|
timingInfo.decodedBodySize += bytes?.byteLength ?? 0
|
|
1855
1849
|
|
|
1856
1850
|
// 6. If bytes is failure, then terminate fetchParams’s controller.
|
|
1857
|
-
if (bytes
|
|
1851
|
+
if (isErrorLike(bytes)) {
|
|
1858
1852
|
fetchParams.controller.terminate(bytes)
|
|
1859
1853
|
return
|
|
1860
1854
|
}
|
|
@@ -1894,7 +1888,7 @@ async function httpNetworkFetch (
|
|
|
1894
1888
|
// 3. Otherwise, if stream is readable, error stream with a TypeError.
|
|
1895
1889
|
if (isReadable(stream)) {
|
|
1896
1890
|
fetchParams.controller.controller.error(new TypeError('terminated', {
|
|
1897
|
-
cause: reason
|
|
1891
|
+
cause: isErrorLike(reason) ? reason : undefined
|
|
1898
1892
|
}))
|
|
1899
1893
|
}
|
|
1900
1894
|
}
|
|
@@ -1942,14 +1936,17 @@ async function httpNetworkFetch (
|
|
|
1942
1936
|
}
|
|
1943
1937
|
|
|
1944
1938
|
let codings = []
|
|
1939
|
+
let location = ''
|
|
1945
1940
|
|
|
1946
1941
|
const headers = new Headers()
|
|
1947
1942
|
for (let n = 0; n < headersList.length; n += 2) {
|
|
1948
|
-
const key = headersList[n + 0].toString()
|
|
1949
|
-
const val = headersList[n + 1].toString()
|
|
1943
|
+
const key = headersList[n + 0].toString('latin1')
|
|
1944
|
+
const val = headersList[n + 1].toString('latin1')
|
|
1950
1945
|
|
|
1951
1946
|
if (key.toLowerCase() === 'content-encoding') {
|
|
1952
1947
|
codings = val.split(',').map((x) => x.trim())
|
|
1948
|
+
} else if (key.toLowerCase() === 'location') {
|
|
1949
|
+
location = val
|
|
1953
1950
|
}
|
|
1954
1951
|
|
|
1955
1952
|
headers.append(key, val)
|
|
@@ -1960,7 +1957,7 @@ async function httpNetworkFetch (
|
|
|
1960
1957
|
const decoders = []
|
|
1961
1958
|
|
|
1962
1959
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
|
|
1963
|
-
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) {
|
|
1960
|
+
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !(request.redirect === 'follow' && location)) {
|
|
1964
1961
|
for (const coding of codings) {
|
|
1965
1962
|
if (/(x-)?gzip/.test(coding)) {
|
|
1966
1963
|
decoders.push(zlib.createGunzip())
|
|
@@ -1980,7 +1977,7 @@ async function httpNetworkFetch (
|
|
|
1980
1977
|
statusText,
|
|
1981
1978
|
headersList: headers[kHeadersList],
|
|
1982
1979
|
body: decoders.length
|
|
1983
|
-
? pipeline(this.body, ...decoders, () => {})
|
|
1980
|
+
? pipeline(this.body, ...decoders, () => { })
|
|
1984
1981
|
: this.body.on('error', () => {})
|
|
1985
1982
|
})
|
|
1986
1983
|
|
package/lib/fetch/request.js
CHANGED
|
@@ -367,9 +367,9 @@ class Request {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
if (signal.aborted) {
|
|
370
|
-
ac.abort()
|
|
370
|
+
ac.abort(signal.reason)
|
|
371
371
|
} else {
|
|
372
|
-
const abort = () => ac.abort()
|
|
372
|
+
const abort = () => ac.abort(signal.reason)
|
|
373
373
|
signal.addEventListener('abort', abort, { once: true })
|
|
374
374
|
requestFinalizer.register(this, { signal, abort })
|
|
375
375
|
}
|
|
@@ -726,12 +726,12 @@ class Request {
|
|
|
726
726
|
// 4. Make clonedRequestObject’s signal follow this’s signal.
|
|
727
727
|
const ac = new AbortController()
|
|
728
728
|
if (this.signal.aborted) {
|
|
729
|
-
ac.abort()
|
|
729
|
+
ac.abort(this.signal.reason)
|
|
730
730
|
} else {
|
|
731
731
|
this.signal.addEventListener(
|
|
732
732
|
'abort',
|
|
733
|
-
|
|
734
|
-
ac.abort()
|
|
733
|
+
() => {
|
|
734
|
+
ac.abort(this.signal.reason)
|
|
735
735
|
},
|
|
736
736
|
{ once: true }
|
|
737
737
|
)
|
package/lib/fetch/response.js
CHANGED
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
isCancelled,
|
|
11
11
|
isAborted,
|
|
12
12
|
isBlobLike,
|
|
13
|
-
serializeJavascriptValueToJSONString
|
|
13
|
+
serializeJavascriptValueToJSONString,
|
|
14
|
+
isErrorLike
|
|
14
15
|
} = require('./util')
|
|
15
16
|
const {
|
|
16
17
|
redirectStatus,
|
|
@@ -347,15 +348,15 @@ function makeResponse (init) {
|
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
function makeNetworkError (reason) {
|
|
351
|
+
const isError = isErrorLike(reason)
|
|
350
352
|
return makeResponse({
|
|
351
353
|
type: 'error',
|
|
352
354
|
status: 0,
|
|
353
|
-
error:
|
|
354
|
-
reason
|
|
355
|
-
|
|
356
|
-
:
|
|
357
|
-
|
|
358
|
-
}),
|
|
355
|
+
error: isError
|
|
356
|
+
? reason
|
|
357
|
+
: new Error(reason ? String(reason) : reason, {
|
|
358
|
+
cause: isError ? reason : undefined
|
|
359
|
+
}),
|
|
359
360
|
aborted: reason && reason.name === 'AbortError'
|
|
360
361
|
})
|
|
361
362
|
}
|
package/lib/fetch/util.js
CHANGED
|
@@ -4,6 +4,8 @@ const { redirectStatus } = require('./constants')
|
|
|
4
4
|
const { performance } = require('perf_hooks')
|
|
5
5
|
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
|
|
6
6
|
const assert = require('assert')
|
|
7
|
+
const { isUint8Array } = require('util/types')
|
|
8
|
+
const { createHash } = require('crypto')
|
|
7
9
|
|
|
8
10
|
let File
|
|
9
11
|
|
|
@@ -82,6 +84,13 @@ function isFileLike (object) {
|
|
|
82
84
|
)
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
function isErrorLike (object) {
|
|
88
|
+
return object instanceof Error || (
|
|
89
|
+
object?.constructor?.name === 'Error' ||
|
|
90
|
+
object?.constructor?.name === 'DOMException'
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
// Check whether |statusText| is a ByteString and
|
|
86
95
|
// matches the Reason-Phrase token production.
|
|
87
96
|
// RFC 2616: https://tools.ietf.org/html/rfc2616
|
|
@@ -333,7 +342,8 @@ function determineRequestsReferrer (request) {
|
|
|
333
342
|
}
|
|
334
343
|
|
|
335
344
|
function matchRequestIntegrity (request, bytes) {
|
|
336
|
-
|
|
345
|
+
const [algo, expectedHashValue] = request.integrity.split('-', 2)
|
|
346
|
+
return createHash(algo).update(bytes).digest('hex') === expectedHashValue
|
|
337
347
|
}
|
|
338
348
|
|
|
339
349
|
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
|
|
@@ -431,6 +441,53 @@ function makeIterator (iterator, name) {
|
|
|
431
441
|
return Object.setPrototypeOf({}, i)
|
|
432
442
|
}
|
|
433
443
|
|
|
444
|
+
/**
|
|
445
|
+
* @see https://fetch.spec.whatwg.org/#body-fully-read
|
|
446
|
+
*/
|
|
447
|
+
async function fullyReadBody (body, processBody, processBodyError) {
|
|
448
|
+
// 1. If taskDestination is null, then set taskDestination to
|
|
449
|
+
// the result of starting a new parallel queue.
|
|
450
|
+
|
|
451
|
+
// 2. Let promise be the result of fully reading body as promise
|
|
452
|
+
// given body.
|
|
453
|
+
try {
|
|
454
|
+
/** @type {Uint8Array[]} */
|
|
455
|
+
const chunks = []
|
|
456
|
+
let length = 0
|
|
457
|
+
|
|
458
|
+
const reader = body.stream.getReader()
|
|
459
|
+
|
|
460
|
+
while (true) {
|
|
461
|
+
const { done, value } = await reader.read()
|
|
462
|
+
|
|
463
|
+
if (done === true) {
|
|
464
|
+
break
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// read-loop chunk steps
|
|
468
|
+
assert(isUint8Array(value))
|
|
469
|
+
|
|
470
|
+
chunks.push(value)
|
|
471
|
+
length += value.byteLength
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 3. Let fulfilledSteps given a byte sequence bytes be to queue
|
|
475
|
+
// a fetch task to run processBody given bytes, with
|
|
476
|
+
// taskDestination.
|
|
477
|
+
const fulfilledSteps = (bytes) => queueMicrotask(() => {
|
|
478
|
+
processBody(bytes)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
fulfilledSteps(Buffer.concat(chunks, length))
|
|
482
|
+
} catch (err) {
|
|
483
|
+
// 4. Let rejectedSteps be to queue a fetch task to run
|
|
484
|
+
// processBodyError, with taskDestination.
|
|
485
|
+
queueMicrotask(() => processBodyError(err))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 5. React to promise with fulfilledSteps and rejectedSteps.
|
|
489
|
+
}
|
|
490
|
+
|
|
434
491
|
/**
|
|
435
492
|
* Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0.
|
|
436
493
|
*/
|
|
@@ -469,5 +526,7 @@ module.exports = {
|
|
|
469
526
|
makeIterator,
|
|
470
527
|
isValidHeaderName,
|
|
471
528
|
isValidHeaderValue,
|
|
472
|
-
hasOwn
|
|
529
|
+
hasOwn,
|
|
530
|
+
isErrorLike,
|
|
531
|
+
fullyReadBody
|
|
473
532
|
}
|
package/lib/fetch/webidl.js
CHANGED
|
@@ -388,9 +388,6 @@ webidl.converters.DOMString = function (V, opts = {}) {
|
|
|
388
388
|
return String(V)
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
// eslint-disable-next-line no-control-regex
|
|
392
|
-
const isNotLatin1 = /[^\u0000-\u00ff]/
|
|
393
|
-
|
|
394
391
|
// https://webidl.spec.whatwg.org/#es-ByteString
|
|
395
392
|
webidl.converters.ByteString = function (V) {
|
|
396
393
|
// 1. Let x be ? ToString(V).
|
|
@@ -399,8 +396,15 @@ webidl.converters.ByteString = function (V) {
|
|
|
399
396
|
|
|
400
397
|
// 2. If the value of any element of x is greater than
|
|
401
398
|
// 255, then throw a TypeError.
|
|
402
|
-
|
|
403
|
-
|
|
399
|
+
for (let index = 0; index < x.length; index++) {
|
|
400
|
+
const charCode = x.charCodeAt(index)
|
|
401
|
+
|
|
402
|
+
if (charCode > 255) {
|
|
403
|
+
throw new TypeError(
|
|
404
|
+
'Cannot convert argument to a ByteString because the character at' +
|
|
405
|
+
`index ${index} has a value of ${charCode} which is greater than 255.`
|
|
406
|
+
)
|
|
407
|
+
}
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
// 3. Return an IDL ByteString value whose length is the
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -38,7 +38,7 @@ function lowerCaseEntries (headers) {
|
|
|
38
38
|
function getHeaderByName (headers, key) {
|
|
39
39
|
if (Array.isArray(headers)) {
|
|
40
40
|
for (let i = 0; i < headers.length; i += 2) {
|
|
41
|
-
if (headers[i] === key) {
|
|
41
|
+
if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
|
|
42
42
|
return headers[i + 1]
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -47,19 +47,24 @@ function getHeaderByName (headers, key) {
|
|
|
47
47
|
} else if (typeof headers.get === 'function') {
|
|
48
48
|
return headers.get(key)
|
|
49
49
|
} else {
|
|
50
|
-
return headers[key]
|
|
50
|
+
return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** @param {string[]} headers */
|
|
55
|
+
function buildHeadersFromArray (headers) { // fetch HeadersList
|
|
56
|
+
const clone = headers.slice()
|
|
57
|
+
const entries = []
|
|
58
|
+
for (let index = 0; index < clone.length; index += 2) {
|
|
59
|
+
entries.push([clone[index], clone[index + 1]])
|
|
60
|
+
}
|
|
61
|
+
return Object.fromEntries(entries)
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
function matchHeaders (mockDispatch, headers) {
|
|
55
65
|
if (typeof mockDispatch.headers === 'function') {
|
|
56
66
|
if (Array.isArray(headers)) { // fetch HeadersList
|
|
57
|
-
|
|
58
|
-
const entries = []
|
|
59
|
-
for (let index = 0; index < clone.length; index += 2) {
|
|
60
|
-
entries.push([clone[index], clone[index + 1]])
|
|
61
|
-
}
|
|
62
|
-
headers = Object.fromEntries(entries)
|
|
67
|
+
headers = buildHeadersFromArray(headers)
|
|
63
68
|
}
|
|
64
69
|
return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
|
|
65
70
|
}
|
|
@@ -284,7 +289,13 @@ function mockDispatch (opts, handler) {
|
|
|
284
289
|
}
|
|
285
290
|
|
|
286
291
|
function handleReply (mockDispatches) {
|
|
287
|
-
|
|
292
|
+
// fetch's HeadersList is a 1D string array
|
|
293
|
+
const optsHeaders = Array.isArray(opts.headers)
|
|
294
|
+
? buildHeadersFromArray(opts.headers)
|
|
295
|
+
: opts.headers
|
|
296
|
+
const responseData = getResponseData(
|
|
297
|
+
typeof data === 'function' ? data({ ...opts, headers: optsHeaders }) : data
|
|
298
|
+
)
|
|
288
299
|
const responseHeaders = generateKeyValues(headers)
|
|
289
300
|
const responseTrailers = generateKeyValues(trailers)
|
|
290
301
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@sinonjs/fake-timers": "^9.1.2",
|
|
68
68
|
"@types/node": "^17.0.29",
|
|
69
69
|
"abort-controller": "^3.0.0",
|
|
70
|
+
"atomic-sleep": "^1.0.0",
|
|
70
71
|
"busboy": "^1.6.0",
|
|
71
72
|
"chai": "^4.3.4",
|
|
72
73
|
"chai-as-promised": "^7.1.1",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Pool = require("./pool")
|
|
2
|
+
|
|
3
|
+
export = PoolStats
|
|
4
|
+
|
|
5
|
+
declare class PoolStats {
|
|
6
|
+
constructor(pool: Pool);
|
|
7
|
+
/** Number of open socket connections in this pool. */
|
|
8
|
+
connected: number;
|
|
9
|
+
/** Number of open socket connections in this pool that do not have an active request. */
|
|
10
|
+
free: number;
|
|
11
|
+
/** Number of pending requests across all clients in this pool. */
|
|
12
|
+
pending: number;
|
|
13
|
+
/** Number of queued requests across all clients in this pool. */
|
|
14
|
+
queued: number;
|
|
15
|
+
/** Number of currently active requests across all clients in this pool. */
|
|
16
|
+
running: number;
|
|
17
|
+
/** Number of active, pending, or queued requests across all clients in this pool. */
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
package/types/pool.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Client = require('./client')
|
|
2
2
|
import Dispatcher = require('./dispatcher')
|
|
3
|
+
import TPoolStats = require('./pool-stats')
|
|
3
4
|
import { URL } from 'url'
|
|
4
5
|
|
|
5
6
|
export = Pool
|
|
@@ -10,9 +11,12 @@ declare class Pool extends Dispatcher {
|
|
|
10
11
|
closed: boolean;
|
|
11
12
|
/** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */
|
|
12
13
|
destroyed: boolean;
|
|
14
|
+
/** Aggregate stats for a Pool. */
|
|
15
|
+
readonly stats: TPoolStats;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
declare namespace Pool {
|
|
19
|
+
export type PoolStats = TPoolStats;
|
|
16
20
|
export interface Options extends Client.Options {
|
|
17
21
|
/** Default: `(origin, opts) => new Client(origin, opts)`. */
|
|
18
22
|
factory?(origin: URL, opts: object): Dispatcher;
|