undici 7.15.0 → 7.17.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 +48 -2
- package/docs/docs/api/Agent.md +1 -0
- package/docs/docs/api/Client.md +1 -0
- package/docs/docs/api/DiagnosticsChannel.md +57 -0
- package/docs/docs/api/Dispatcher.md +86 -0
- package/docs/docs/api/Errors.md +0 -1
- package/docs/docs/api/RoundRobinPool.md +145 -0
- package/docs/docs/api/WebSocket.md +21 -0
- package/docs/docs/best-practices/crawling.md +58 -0
- package/index-fetch.js +2 -2
- package/index.js +8 -9
- package/lib/api/api-request.js +22 -8
- package/lib/api/api-upgrade.js +2 -1
- package/lib/api/readable.js +7 -5
- package/lib/core/connect.js +4 -1
- package/lib/core/diagnostics.js +28 -1
- package/lib/core/errors.js +217 -13
- package/lib/core/request.js +5 -1
- package/lib/core/symbols.js +3 -0
- package/lib/core/util.js +61 -41
- package/lib/dispatcher/agent.js +19 -7
- package/lib/dispatcher/balanced-pool.js +10 -0
- package/lib/dispatcher/client-h1.js +18 -23
- package/lib/dispatcher/client-h2.js +166 -26
- package/lib/dispatcher/client.js +64 -59
- package/lib/dispatcher/dispatcher-base.js +20 -16
- package/lib/dispatcher/env-http-proxy-agent.js +12 -16
- package/lib/dispatcher/fixed-queue.js +15 -39
- package/lib/dispatcher/h2c-client.js +7 -78
- package/lib/dispatcher/pool-base.js +60 -43
- package/lib/dispatcher/pool.js +2 -2
- package/lib/dispatcher/proxy-agent.js +27 -11
- package/lib/dispatcher/round-robin-pool.js +137 -0
- package/lib/encoding/index.js +33 -0
- package/lib/global.js +19 -1
- package/lib/handler/cache-handler.js +84 -27
- package/lib/handler/deduplication-handler.js +216 -0
- package/lib/handler/retry-handler.js +0 -2
- package/lib/interceptor/cache.js +94 -15
- package/lib/interceptor/decompress.js +2 -1
- package/lib/interceptor/deduplicate.js +109 -0
- package/lib/interceptor/dns.js +55 -13
- package/lib/mock/mock-agent.js +4 -4
- package/lib/mock/mock-errors.js +10 -0
- package/lib/mock/mock-utils.js +13 -12
- package/lib/mock/snapshot-agent.js +11 -5
- package/lib/mock/snapshot-recorder.js +12 -4
- package/lib/mock/snapshot-utils.js +4 -4
- package/lib/util/cache.js +29 -1
- package/lib/util/date.js +534 -140
- package/lib/util/runtime-features.js +124 -0
- package/lib/web/cookies/index.js +1 -1
- package/lib/web/cookies/parse.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 +45 -61
- package/lib/web/fetch/data-url.js +12 -160
- package/lib/web/fetch/formdata-parser.js +204 -127
- package/lib/web/fetch/index.js +21 -19
- package/lib/web/fetch/request.js +6 -0
- package/lib/web/fetch/response.js +4 -7
- package/lib/web/fetch/util.js +10 -79
- package/lib/web/infra/index.js +229 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +6 -5
- package/lib/web/webidl/index.js +207 -44
- package/lib/web/websocket/connection.js +33 -22
- package/lib/web/websocket/events.js +1 -1
- package/lib/web/websocket/frame.js +9 -15
- package/lib/web/websocket/stream/websocketerror.js +22 -1
- package/lib/web/websocket/stream/websocketstream.js +17 -8
- package/lib/web/websocket/util.js +2 -1
- package/lib/web/websocket/websocket.js +32 -42
- package/package.json +9 -7
- package/types/agent.d.ts +2 -1
- package/types/api.d.ts +2 -2
- package/types/balanced-pool.d.ts +2 -1
- package/types/cache-interceptor.d.ts +1 -0
- package/types/client.d.ts +1 -1
- package/types/connector.d.ts +2 -2
- package/types/diagnostics-channel.d.ts +2 -2
- package/types/dispatcher.d.ts +12 -12
- package/types/errors.d.ts +5 -15
- package/types/fetch.d.ts +4 -4
- package/types/formdata.d.ts +1 -1
- package/types/h2c-client.d.ts +1 -1
- package/types/index.d.ts +9 -1
- package/types/interceptors.d.ts +36 -2
- package/types/pool.d.ts +1 -1
- package/types/readable.d.ts +2 -2
- package/types/round-robin-pool.d.ts +41 -0
- package/types/webidl.d.ts +82 -21
- package/types/websocket.d.ts +9 -9
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
|
@@ -337,8 +337,7 @@ function mockDispatch (opts, handler) {
|
|
|
337
337
|
// synchronously throw the error, which breaks some tests.
|
|
338
338
|
// Rather, we wait for the callback to resolve if it is a
|
|
339
339
|
// promise, and then re-run handleReply with the new body.
|
|
340
|
-
body.then((newData) => handleReply(mockDispatches, newData))
|
|
341
|
-
return
|
|
340
|
+
return body.then((newData) => handleReply(mockDispatches, newData))
|
|
342
341
|
}
|
|
343
342
|
|
|
344
343
|
const responseData = getResponseData(body)
|
|
@@ -367,7 +366,7 @@ function buildMockDispatch () {
|
|
|
367
366
|
try {
|
|
368
367
|
mockDispatch.call(this, opts, handler)
|
|
369
368
|
} catch (error) {
|
|
370
|
-
if (error
|
|
369
|
+
if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
|
|
371
370
|
const netConnect = agent[kGetNetConnect]()
|
|
372
371
|
if (netConnect === false) {
|
|
373
372
|
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
|
|
@@ -398,19 +397,21 @@ function checkNetConnect (netConnect, origin) {
|
|
|
398
397
|
}
|
|
399
398
|
|
|
400
399
|
function buildAndValidateMockOptions (opts) {
|
|
401
|
-
|
|
402
|
-
const { agent, ...mockOptions } = opts
|
|
400
|
+
const { agent, ...mockOptions } = opts
|
|
403
401
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
402
|
+
if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
|
|
403
|
+
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
|
|
404
|
+
}
|
|
407
405
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
406
|
+
if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') {
|
|
407
|
+
throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean')
|
|
408
|
+
}
|
|
411
409
|
|
|
412
|
-
|
|
410
|
+
if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') {
|
|
411
|
+
throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean')
|
|
413
412
|
}
|
|
413
|
+
|
|
414
|
+
return mockOptions
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
module.exports = {
|
|
@@ -64,7 +64,9 @@ class SnapshotAgent extends MockAgent {
|
|
|
64
64
|
this[kSnapshotLoaded] = false
|
|
65
65
|
|
|
66
66
|
// For recording/update mode, we need a real agent to make actual requests
|
|
67
|
-
|
|
67
|
+
// For playback mode, we need a real agent if there are excluded URLs
|
|
68
|
+
if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' ||
|
|
69
|
+
(this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) {
|
|
68
70
|
this[kRealAgent] = new Agent(opts)
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -80,6 +82,12 @@ class SnapshotAgent extends MockAgent {
|
|
|
80
82
|
handler = WrapHandler.wrap(handler)
|
|
81
83
|
const mode = this[kSnapshotMode]
|
|
82
84
|
|
|
85
|
+
// Check if URL should be excluded (pass through without mocking/recording)
|
|
86
|
+
if (this[kSnapshotRecorder].isUrlExcluded(opts)) {
|
|
87
|
+
// Real agent is guaranteed by constructor when excludeUrls is configured
|
|
88
|
+
return this[kRealAgent].dispatch(opts, handler)
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
if (mode === 'playback' || mode === 'update') {
|
|
84
92
|
// Ensure snapshots are loaded
|
|
85
93
|
if (!this[kSnapshotLoaded]) {
|
|
@@ -162,11 +170,9 @@ class SnapshotAgent extends MockAgent {
|
|
|
162
170
|
headers: responseData.headers,
|
|
163
171
|
body: responseBody,
|
|
164
172
|
trailers: responseData.trailers
|
|
165
|
-
}).then(() => {
|
|
166
|
-
handler.onResponseEnd(controller, trailers)
|
|
167
|
-
}).catch((error) => {
|
|
168
|
-
handler.onResponseError(controller, error)
|
|
169
173
|
})
|
|
174
|
+
.then(() => handler.onResponseEnd(controller, trailers))
|
|
175
|
+
.catch((error) => handler.onResponseError(controller, error))
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -283,8 +283,7 @@ class SnapshotRecorder {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// Check URL exclusion patterns
|
|
286
|
-
|
|
287
|
-
if (this.#isUrlExcluded(url)) {
|
|
286
|
+
if (this.isUrlExcluded(requestOpts)) {
|
|
288
287
|
return // Skip recording
|
|
289
288
|
}
|
|
290
289
|
|
|
@@ -330,6 +329,16 @@ class SnapshotRecorder {
|
|
|
330
329
|
}
|
|
331
330
|
}
|
|
332
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Checks if a URL should be excluded from recording/playback
|
|
334
|
+
* @param {SnapshotRequestOptions} requestOpts - Request options to check
|
|
335
|
+
* @returns {boolean} - True if URL is excluded
|
|
336
|
+
*/
|
|
337
|
+
isUrlExcluded (requestOpts) {
|
|
338
|
+
const url = new URL(requestOpts.path, requestOpts.origin).toString()
|
|
339
|
+
return this.#isUrlExcluded(url)
|
|
340
|
+
}
|
|
341
|
+
|
|
333
342
|
/**
|
|
334
343
|
* Finds a matching snapshot for the given request
|
|
335
344
|
* Returns the appropriate response based on call count for sequential responses
|
|
@@ -344,8 +353,7 @@ class SnapshotRecorder {
|
|
|
344
353
|
}
|
|
345
354
|
|
|
346
355
|
// Check URL exclusion patterns
|
|
347
|
-
|
|
348
|
-
if (this.#isUrlExcluded(url)) {
|
|
356
|
+
if (this.isUrlExcluded(requestOpts)) {
|
|
349
357
|
return undefined // Skip playback
|
|
350
358
|
}
|
|
351
359
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { InvalidArgumentError } = require('../core/errors')
|
|
4
|
+
const { runtimeFeatures } = require('../util/runtime-features.js')
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {Object} HeaderFilters
|
|
@@ -25,10 +26,9 @@ function createHeaderFilters (matchOptions = {}) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} catch { /* Fallback if crypto is not available */ }
|
|
29
|
+
const crypto = runtimeFeatures.has('crypto')
|
|
30
|
+
? require('node:crypto')
|
|
31
|
+
: null
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* @callback HashIdFunction
|
package/lib/util/cache.js
CHANGED
|
@@ -364,6 +364,33 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Creates a string key for request deduplication purposes.
|
|
369
|
+
* This key is used to identify in-flight requests that can be shared.
|
|
370
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
371
|
+
* @param {Set<string>} [excludeHeaders] Set of lowercase header names to exclude from the key
|
|
372
|
+
* @returns {string}
|
|
373
|
+
*/
|
|
374
|
+
function makeDeduplicationKey (cacheKey, excludeHeaders) {
|
|
375
|
+
// Create a deterministic string key from the cache key
|
|
376
|
+
// Include origin, method, path, and sorted headers
|
|
377
|
+
let key = `${cacheKey.origin}:${cacheKey.method}:${cacheKey.path}`
|
|
378
|
+
|
|
379
|
+
if (cacheKey.headers) {
|
|
380
|
+
const sortedHeaders = Object.keys(cacheKey.headers).sort()
|
|
381
|
+
for (const header of sortedHeaders) {
|
|
382
|
+
// Skip excluded headers
|
|
383
|
+
if (excludeHeaders?.has(header.toLowerCase())) {
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
const value = cacheKey.headers[header]
|
|
387
|
+
key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return key
|
|
392
|
+
}
|
|
393
|
+
|
|
367
394
|
module.exports = {
|
|
368
395
|
makeCacheKey,
|
|
369
396
|
normalizeHeaders,
|
|
@@ -373,5 +400,6 @@ module.exports = {
|
|
|
373
400
|
parseVaryHeader,
|
|
374
401
|
isEtagUsable,
|
|
375
402
|
assertCacheMethods,
|
|
376
|
-
assertCacheStore
|
|
403
|
+
assertCacheStore,
|
|
404
|
+
makeDeduplicationKey
|
|
377
405
|
}
|