undici 7.15.0 → 7.16.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 +1 -1
- package/docs/docs/api/Agent.md +1 -0
- package/docs/docs/api/Errors.md +0 -1
- package/index-fetch.js +2 -2
- package/index.js +4 -8
- package/lib/api/api-request.js +22 -8
- package/lib/api/readable.js +7 -5
- package/lib/core/errors.js +217 -13
- package/lib/core/request.js +5 -1
- package/lib/core/util.js +32 -10
- package/lib/dispatcher/agent.js +19 -7
- package/lib/dispatcher/client-h1.js +20 -9
- package/lib/dispatcher/client-h2.js +13 -3
- package/lib/dispatcher/client.js +57 -57
- package/lib/dispatcher/dispatcher-base.js +12 -7
- package/lib/dispatcher/env-http-proxy-agent.js +12 -16
- package/lib/dispatcher/fixed-queue.js +15 -39
- package/lib/dispatcher/h2c-client.js +6 -6
- package/lib/dispatcher/pool-base.js +60 -43
- package/lib/dispatcher/pool.js +2 -2
- package/lib/dispatcher/proxy-agent.js +14 -9
- package/lib/global.js +19 -1
- package/lib/interceptor/cache.js +61 -0
- package/lib/mock/mock-agent.js +4 -4
- package/lib/mock/mock-errors.js +10 -0
- package/lib/mock/mock-utils.js +12 -10
- package/lib/util/date.js +534 -140
- package/lib/web/cookies/index.js +1 -1
- package/lib/web/eventsource/eventsource-stream.js +2 -2
- package/lib/web/eventsource/eventsource.js +34 -29
- package/lib/web/eventsource/util.js +1 -9
- package/lib/web/fetch/body.js +16 -22
- package/lib/web/fetch/index.js +14 -15
- package/lib/web/fetch/response.js +2 -4
- package/lib/web/fetch/util.js +8 -14
- package/lib/web/webidl/index.js +203 -42
- package/lib/web/websocket/connection.js +4 -3
- package/lib/web/websocket/events.js +1 -1
- package/lib/web/websocket/stream/websocketerror.js +22 -1
- package/lib/web/websocket/stream/websocketstream.js +16 -7
- package/lib/web/websocket/websocket.js +32 -42
- package/package.json +7 -6
- package/types/agent.d.ts +1 -0
- package/types/errors.d.ts +5 -15
- package/types/webidl.d.ts +82 -21
|
@@ -18,54 +18,55 @@ const kAddClient = Symbol('add client')
|
|
|
18
18
|
const kRemoveClient = Symbol('remove client')
|
|
19
19
|
|
|
20
20
|
class PoolBase extends DispatcherBase {
|
|
21
|
-
|
|
22
|
-
super()
|
|
21
|
+
[kQueue] = new FixedQueue();
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
this[kClients] = []
|
|
26
|
-
this[kQueued] = 0
|
|
23
|
+
[kQueued] = 0;
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
[kClients] = [];
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
const queue = pool[kQueue]
|
|
27
|
+
[kNeedDrain] = false;
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
[kOnDrain] (client, origin, targets) {
|
|
30
|
+
const queue = this[kQueue]
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
needDrain = !this.dispatch(item.opts, item.handler)
|
|
32
|
+
let needDrain = false
|
|
33
|
+
|
|
34
|
+
while (!needDrain) {
|
|
35
|
+
const item = queue.shift()
|
|
36
|
+
if (!item) {
|
|
37
|
+
break
|
|
42
38
|
}
|
|
39
|
+
this[kQueued]--
|
|
40
|
+
needDrain = !client.dispatch(item.opts, item.handler)
|
|
41
|
+
}
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
client[kNeedDrain] = needDrain
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
if (!needDrain && this[kNeedDrain]) {
|
|
46
|
+
this[kNeedDrain] = false
|
|
47
|
+
this.emit('drain', origin, [this, ...targets])
|
|
48
|
+
}
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (this[kClosedResolve] && queue.isEmpty()) {
|
|
51
|
+
const closeAll = new Array(this[kClients].length)
|
|
52
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
53
|
+
closeAll[i] = this[kClients][i].close()
|
|
55
54
|
}
|
|
55
|
+
Promise.all(closeAll)
|
|
56
|
+
.then(this[kClosedResolve])
|
|
56
57
|
}
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
[kOnConnect] = (origin, targets) => {
|
|
61
|
+
this.emit('connect', origin, [this, ...targets])
|
|
62
|
+
};
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
[kOnDisconnect] = (origin, targets, err) => {
|
|
65
|
+
this.emit('disconnect', origin, [this, ...targets], err)
|
|
66
|
+
};
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
68
|
+
[kOnConnectionError] = (origin, targets, err) => {
|
|
69
|
+
this.emit('connectionError', origin, [this, ...targets], err)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
get [kBusy] () {
|
|
@@ -73,11 +74,19 @@ class PoolBase extends DispatcherBase {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
get [kConnected] () {
|
|
76
|
-
|
|
77
|
+
let ret = 0
|
|
78
|
+
for (const { [kConnected]: connected } of this[kClients]) {
|
|
79
|
+
ret += connected
|
|
80
|
+
}
|
|
81
|
+
return ret
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
get [kFree] () {
|
|
80
|
-
|
|
85
|
+
let ret = 0
|
|
86
|
+
for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) {
|
|
87
|
+
ret += connected && !needDrain
|
|
88
|
+
}
|
|
89
|
+
return ret
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
get [kPending] () {
|
|
@@ -108,17 +117,21 @@ class PoolBase extends DispatcherBase {
|
|
|
108
117
|
return new PoolStats(this)
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
[kClose] () {
|
|
112
121
|
if (this[kQueue].isEmpty()) {
|
|
113
|
-
|
|
122
|
+
const closeAll = new Array(this[kClients].length)
|
|
123
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
124
|
+
closeAll[i] = this[kClients][i].close()
|
|
125
|
+
}
|
|
126
|
+
return Promise.all(closeAll)
|
|
114
127
|
} else {
|
|
115
|
-
|
|
128
|
+
return new Promise((resolve) => {
|
|
116
129
|
this[kClosedResolve] = resolve
|
|
117
130
|
})
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
[kDestroy] (err) {
|
|
122
135
|
while (true) {
|
|
123
136
|
const item = this[kQueue].shift()
|
|
124
137
|
if (!item) {
|
|
@@ -127,7 +140,11 @@ class PoolBase extends DispatcherBase {
|
|
|
127
140
|
item.handler.onError(err)
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
|
|
143
|
+
const destroyAll = new Array(this[kClients].length)
|
|
144
|
+
for (let i = 0; i < this[kClients].length; i++) {
|
|
145
|
+
destroyAll[i] = this[kClients][i].destroy(err)
|
|
146
|
+
}
|
|
147
|
+
return Promise.all(destroyAll)
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
[kDispatch] (opts, handler) {
|
|
@@ -147,7 +164,7 @@ class PoolBase extends DispatcherBase {
|
|
|
147
164
|
|
|
148
165
|
[kAddClient] (client) {
|
|
149
166
|
client
|
|
150
|
-
.on('drain', this[kOnDrain])
|
|
167
|
+
.on('drain', this[kOnDrain].bind(this, client))
|
|
151
168
|
.on('connect', this[kOnConnect])
|
|
152
169
|
.on('disconnect', this[kOnDisconnect])
|
|
153
170
|
.on('connectionError', this[kOnConnectionError])
|
|
@@ -157,7 +174,7 @@ class PoolBase extends DispatcherBase {
|
|
|
157
174
|
if (this[kNeedDrain]) {
|
|
158
175
|
queueMicrotask(() => {
|
|
159
176
|
if (this[kNeedDrain]) {
|
|
160
|
-
this[kOnDrain](client[kUrl], [
|
|
177
|
+
this[kOnDrain](client, client[kUrl], [client, this])
|
|
161
178
|
}
|
|
162
179
|
})
|
|
163
180
|
}
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -51,8 +51,6 @@ class Pool extends PoolBase {
|
|
|
51
51
|
throw new InvalidArgumentError('connect must be a function or an object')
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
super()
|
|
55
|
-
|
|
56
54
|
if (typeof connect !== 'function') {
|
|
57
55
|
connect = buildConnector({
|
|
58
56
|
...tls,
|
|
@@ -65,6 +63,8 @@ class Pool extends PoolBase {
|
|
|
65
63
|
})
|
|
66
64
|
}
|
|
67
65
|
|
|
66
|
+
super()
|
|
67
|
+
|
|
68
68
|
this[kConnections] = connections || null
|
|
69
69
|
this[kUrl] = util.parseOrigin(origin)
|
|
70
70
|
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
|
|
@@ -37,11 +37,12 @@ class Http1ProxyWrapper extends DispatcherBase {
|
|
|
37
37
|
#client
|
|
38
38
|
|
|
39
39
|
constructor (proxyUrl, { headers = {}, connect, factory }) {
|
|
40
|
-
super()
|
|
41
40
|
if (!proxyUrl) {
|
|
42
41
|
throw new InvalidArgumentError('Proxy URL is mandatory')
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
super()
|
|
45
|
+
|
|
45
46
|
this[kProxyHeaders] = headers
|
|
46
47
|
if (factory) {
|
|
47
48
|
this.#client = factory(proxyUrl, { connect })
|
|
@@ -80,11 +81,11 @@ class Http1ProxyWrapper extends DispatcherBase {
|
|
|
80
81
|
return this.#client[kDispatch](opts, handler)
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
[kClose] () {
|
|
84
85
|
return this.#client.close()
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
[kDestroy] (err) {
|
|
88
89
|
return this.#client.destroy(err)
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -220,14 +221,18 @@ class ProxyAgent extends DispatcherBase {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
[kClose] () {
|
|
225
|
+
return Promise.all([
|
|
226
|
+
this[kAgent].close(),
|
|
227
|
+
this[kClient].close()
|
|
228
|
+
])
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
[kDestroy] () {
|
|
232
|
+
return Promise.all([
|
|
233
|
+
this[kAgent].destroy(),
|
|
234
|
+
this[kClient].destroy()
|
|
235
|
+
])
|
|
231
236
|
}
|
|
232
237
|
}
|
|
233
238
|
|
package/lib/global.js
CHANGED
|
@@ -26,7 +26,25 @@ function getGlobalDispatcher () {
|
|
|
26
26
|
return globalThis[globalDispatcher]
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// These are the globals that can be installed by undici.install().
|
|
30
|
+
// Not exported by index.js to avoid use outside of this module.
|
|
31
|
+
const installedExports = /** @type {const} */ (
|
|
32
|
+
[
|
|
33
|
+
'fetch',
|
|
34
|
+
'Headers',
|
|
35
|
+
'Response',
|
|
36
|
+
'Request',
|
|
37
|
+
'FormData',
|
|
38
|
+
'WebSocket',
|
|
39
|
+
'CloseEvent',
|
|
40
|
+
'ErrorEvent',
|
|
41
|
+
'MessageEvent',
|
|
42
|
+
'EventSource'
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
29
46
|
module.exports = {
|
|
30
47
|
setGlobalDispatcher,
|
|
31
|
-
getGlobalDispatcher
|
|
48
|
+
getGlobalDispatcher,
|
|
49
|
+
installedExports
|
|
32
50
|
}
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -56,6 +56,22 @@ function needsRevalidation (result, cacheControlDirectives) {
|
|
|
56
56
|
return false
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Check if we're within the stale-while-revalidate window for a stale response
|
|
61
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function withinStaleWhileRevalidateWindow (result) {
|
|
65
|
+
const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
|
|
66
|
+
if (!staleWhileRevalidate) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const now = Date.now()
|
|
71
|
+
const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
|
|
72
|
+
return now <= staleWhileRevalidateExpiry
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
/**
|
|
60
76
|
* @param {DispatchFn} dispatch
|
|
61
77
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
|
|
@@ -231,6 +247,51 @@ function handleResult (
|
|
|
231
247
|
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
232
248
|
}
|
|
233
249
|
|
|
250
|
+
// RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
|
|
251
|
+
// and revalidate in background
|
|
252
|
+
if (withinStaleWhileRevalidateWindow(result)) {
|
|
253
|
+
// Serve stale response immediately
|
|
254
|
+
sendCachedValue(handler, opts, result, age, null, true)
|
|
255
|
+
|
|
256
|
+
// Start background revalidation (fire-and-forget)
|
|
257
|
+
queueMicrotask(() => {
|
|
258
|
+
let headers = {
|
|
259
|
+
...opts.headers,
|
|
260
|
+
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result.etag) {
|
|
264
|
+
headers['if-none-match'] = result.etag
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (result.vary) {
|
|
268
|
+
headers = {
|
|
269
|
+
...headers,
|
|
270
|
+
...result.vary
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Background revalidation - update cache if we get new data
|
|
275
|
+
dispatch(
|
|
276
|
+
{
|
|
277
|
+
...opts,
|
|
278
|
+
headers
|
|
279
|
+
},
|
|
280
|
+
new CacheHandler(globalOpts, cacheKey, {
|
|
281
|
+
// Silent handler that just updates the cache
|
|
282
|
+
onRequestStart () {},
|
|
283
|
+
onRequestUpgrade () {},
|
|
284
|
+
onResponseStart () {},
|
|
285
|
+
onResponseData () {},
|
|
286
|
+
onResponseEnd () {},
|
|
287
|
+
onResponseError () {}
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
|
|
234
295
|
let withinStaleIfErrorThreshold = false
|
|
235
296
|
const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
|
|
236
297
|
if (staleIfErrorExpiry) {
|
package/lib/mock/mock-agent.js
CHANGED
|
@@ -29,16 +29,16 @@ const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
|
|
|
29
29
|
const { MockCallHistory } = require('./mock-call-history')
|
|
30
30
|
|
|
31
31
|
class MockAgent extends Dispatcher {
|
|
32
|
-
constructor (opts) {
|
|
32
|
+
constructor (opts = {}) {
|
|
33
33
|
super(opts)
|
|
34
34
|
|
|
35
35
|
const mockOptions = buildAndValidateMockOptions(opts)
|
|
36
36
|
|
|
37
37
|
this[kNetConnect] = true
|
|
38
38
|
this[kIsMockActive] = true
|
|
39
|
-
this[kMockAgentIsCallHistoryEnabled] = mockOptions
|
|
40
|
-
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions
|
|
41
|
-
this[kIgnoreTrailingSlash] = mockOptions
|
|
39
|
+
this[kMockAgentIsCallHistoryEnabled] = mockOptions.enableCallHistory ?? false
|
|
40
|
+
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions.acceptNonStandardSearchParameters ?? false
|
|
41
|
+
this[kIgnoreTrailingSlash] = mockOptions.ignoreTrailingSlash ?? false
|
|
42
42
|
|
|
43
43
|
// Instantiate Agent and encapsulate
|
|
44
44
|
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
|
package/lib/mock/mock-errors.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { UndiciError } = require('../core/errors')
|
|
4
4
|
|
|
5
|
+
const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED')
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* The request does not match any registered mock dispatches.
|
|
7
9
|
*/
|
|
@@ -12,6 +14,14 @@ class MockNotMatchedError extends UndiciError {
|
|
|
12
14
|
this.message = message || 'The request does not match any registered mock dispatches'
|
|
13
15
|
this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED'
|
|
14
16
|
}
|
|
17
|
+
|
|
18
|
+
static [Symbol.hasInstance] (instance) {
|
|
19
|
+
return instance && instance[kMockNotMatchedError] === true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get [kMockNotMatchedError] () {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
module.exports = {
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -367,7 +367,7 @@ function buildMockDispatch () {
|
|
|
367
367
|
try {
|
|
368
368
|
mockDispatch.call(this, opts, handler)
|
|
369
369
|
} catch (error) {
|
|
370
|
-
if (error
|
|
370
|
+
if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
|
|
371
371
|
const netConnect = agent[kGetNetConnect]()
|
|
372
372
|
if (netConnect === false) {
|
|
373
373
|
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
|
|
@@ -398,19 +398,21 @@ function checkNetConnect (netConnect, origin) {
|
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
function buildAndValidateMockOptions (opts) {
|
|
401
|
-
|
|
402
|
-
const { agent, ...mockOptions } = opts
|
|
401
|
+
const { agent, ...mockOptions } = opts
|
|
403
402
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
403
|
+
if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
|
|
404
|
+
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
|
|
405
|
+
}
|
|
407
406
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
407
|
+
if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') {
|
|
408
|
+
throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean')
|
|
409
|
+
}
|
|
411
410
|
|
|
412
|
-
|
|
411
|
+
if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') {
|
|
412
|
+
throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean')
|
|
413
413
|
}
|
|
414
|
+
|
|
415
|
+
return mockOptions
|
|
414
416
|
}
|
|
415
417
|
|
|
416
418
|
module.exports = {
|