undici 8.2.0 → 8.4.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 +67 -23
- package/docs/docs/api/Agent.md +3 -0
- package/docs/docs/api/Client.md +43 -5
- package/docs/docs/api/Connector.md +1 -0
- package/docs/docs/api/Dispatcher.md +7 -0
- package/docs/docs/api/Errors.md +12 -0
- package/docs/docs/api/EventSource.md +50 -3
- package/docs/docs/api/Fetch.md +3 -1
- package/docs/docs/api/GlobalInstallation.md +7 -5
- package/docs/docs/api/H2CClient.md +2 -2
- package/docs/docs/api/Pool.md +3 -0
- package/docs/docs/api/RedirectHandler.md +4 -1
- package/docs/docs/api/SnapshotAgent.md +23 -0
- package/lib/api/api-pipeline.js +4 -0
- package/lib/api/api-stream.js +51 -5
- package/lib/core/connect.js +29 -4
- package/lib/core/symbols.js +1 -0
- package/lib/core/util.js +10 -8
- package/lib/dispatcher/client-h1.js +59 -18
- package/lib/dispatcher/client-h2.js +418 -298
- package/lib/dispatcher/client.js +25 -4
- package/lib/dispatcher/pool-base.js +21 -3
- package/lib/dispatcher/pool.js +23 -0
- package/lib/dispatcher/proxy-agent.js +21 -4
- package/lib/dispatcher/round-robin-pool.js +26 -0
- package/lib/dispatcher/socks5-proxy-agent.js +19 -19
- package/lib/handler/redirect-handler.js +36 -11
- package/lib/handler/retry-handler.js +14 -0
- package/lib/interceptor/redirect.js +3 -3
- package/lib/mock/mock-call-history.js +1 -1
- package/lib/mock/mock-utils.js +3 -1
- package/lib/mock/snapshot-agent.js +11 -1
- package/lib/mock/snapshot-recorder.js +38 -3
- package/lib/web/fetch/body.js +2 -7
- package/lib/web/fetch/formdata.js +21 -2
- package/lib/web/fetch/index.js +19 -3
- package/lib/web/fetch/request.js +32 -3
- package/package.json +4 -4
- package/types/client.d.ts +7 -7
- package/types/connector.d.ts +1 -0
- package/types/dispatcher.d.ts +0 -2
- package/types/fetch.d.ts +4 -1
- package/types/formdata.d.ts +0 -6
- package/types/interceptors.d.ts +1 -1
- package/types/snapshot-agent.d.ts +4 -0
package/lib/web/fetch/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const {
|
|
|
11
11
|
getResponseState
|
|
12
12
|
} = require('./response')
|
|
13
13
|
const { HeadersList } = require('./headers')
|
|
14
|
-
const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request')
|
|
14
|
+
const { Request, cloneRequest, getRequestDispatcher, getRequestState, removeRequestAbortListener } = require('./request')
|
|
15
15
|
const zlib = require('node:zlib')
|
|
16
16
|
const {
|
|
17
17
|
makePolicyContainer,
|
|
@@ -208,7 +208,7 @@ function fetch (input, init = undefined) {
|
|
|
208
208
|
let controller = null
|
|
209
209
|
|
|
210
210
|
// 11. Add the following abort steps to requestObject’s signal:
|
|
211
|
-
addAbortListener(
|
|
211
|
+
const removeAbortListener = addAbortListener(
|
|
212
212
|
requestObject.signal,
|
|
213
213
|
() => {
|
|
214
214
|
// 1. Set locallyAborted to true.
|
|
@@ -228,6 +228,15 @@ function fetch (input, init = undefined) {
|
|
|
228
228
|
}
|
|
229
229
|
)
|
|
230
230
|
|
|
231
|
+
// Remove the `abort` listeners registered above and in the Request
|
|
232
|
+
// constructor once the fetch has settled. Without this, reusing a single
|
|
233
|
+
// signal across many requests leaks listeners and Node.js emits a
|
|
234
|
+
// MaxListenersExceededWarning. See https://github.com/nodejs/undici/issues/5285
|
|
235
|
+
const cleanupAbortListeners = () => {
|
|
236
|
+
removeAbortListener()
|
|
237
|
+
removeRequestAbortListener(requestObject)
|
|
238
|
+
}
|
|
239
|
+
|
|
231
240
|
// 12. Let handleFetchDone given response response be to finalize and
|
|
232
241
|
// report timing with response, globalObject, and "fetch".
|
|
233
242
|
// see function handleFetchDone
|
|
@@ -252,6 +261,7 @@ function fetch (input, init = undefined) {
|
|
|
252
261
|
// deserializedError.
|
|
253
262
|
|
|
254
263
|
abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller)
|
|
264
|
+
cleanupAbortListeners()
|
|
255
265
|
return
|
|
256
266
|
}
|
|
257
267
|
|
|
@@ -259,6 +269,7 @@ function fetch (input, init = undefined) {
|
|
|
259
269
|
// and terminate these substeps.
|
|
260
270
|
if (response.type === 'error') {
|
|
261
271
|
p.reject(new TypeError('fetch failed', { cause: response.error }))
|
|
272
|
+
cleanupAbortListeners()
|
|
262
273
|
return
|
|
263
274
|
}
|
|
264
275
|
|
|
@@ -273,7 +284,10 @@ function fetch (input, init = undefined) {
|
|
|
273
284
|
|
|
274
285
|
controller = fetching({
|
|
275
286
|
request,
|
|
276
|
-
processResponseEndOfBody:
|
|
287
|
+
processResponseEndOfBody: (response) => {
|
|
288
|
+
handleFetchDone(response)
|
|
289
|
+
cleanupAbortListeners()
|
|
290
|
+
},
|
|
277
291
|
processResponse,
|
|
278
292
|
dispatcher: getRequestDispatcher(requestObject), // undici
|
|
279
293
|
// Keep requestObject alive to prevent its AbortController from being GC'd
|
|
@@ -2184,6 +2198,8 @@ async function httpNetworkFetch (
|
|
|
2184
2198
|
origin: url.origin,
|
|
2185
2199
|
method: request.method,
|
|
2186
2200
|
body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
|
|
2201
|
+
// Preserve the serialized fetch body for MockAgent net-connect fallthroughs.
|
|
2202
|
+
__mockAgentBodyForDispatch: body,
|
|
2187
2203
|
headers: request.headersList.entries,
|
|
2188
2204
|
maxRedirections: 0,
|
|
2189
2205
|
upgrade: request.mode === 'websocket' ? 'websocket' : undefined,
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -97,6 +97,13 @@ class Request {
|
|
|
97
97
|
|
|
98
98
|
#state
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Removes the `abort` listener that makes this request's signal follow the
|
|
102
|
+
* passed signal. `null` when no such listener was registered.
|
|
103
|
+
* @type {(() => void) | null}
|
|
104
|
+
*/
|
|
105
|
+
#abortCleanup = null
|
|
106
|
+
|
|
100
107
|
// https://fetch.spec.whatwg.org/#dom-request
|
|
101
108
|
constructor (input, init = undefined) {
|
|
102
109
|
webidl.util.markAsUncloneable(this)
|
|
@@ -436,12 +443,23 @@ class Request {
|
|
|
436
443
|
setMaxListeners(1500, signal)
|
|
437
444
|
}
|
|
438
445
|
|
|
439
|
-
util.addAbortListener(signal, abort)
|
|
446
|
+
const removeAbortListener = util.addAbortListener(signal, abort)
|
|
440
447
|
// The third argument must be a registry key to be unregistered.
|
|
441
448
|
// Without it, you cannot unregister.
|
|
442
449
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
|
|
443
450
|
// abort is used as the unregister key. (because it is unique)
|
|
444
451
|
requestFinalizer.register(ac, { signal, abort }, abort)
|
|
452
|
+
|
|
453
|
+
// Allow the listener to be removed deterministically once the fetch
|
|
454
|
+
// that owns this request has settled, instead of relying solely on the
|
|
455
|
+
// FinalizationRegistry (i.e. garbage collection). Reusing a single
|
|
456
|
+
// signal across many requests would otherwise leak listeners.
|
|
457
|
+
// See https://github.com/nodejs/undici/issues/5285
|
|
458
|
+
this.#abortCleanup = () => {
|
|
459
|
+
requestFinalizer.unregister(abort)
|
|
460
|
+
removeAbortListener()
|
|
461
|
+
this.#abortCleanup = null
|
|
462
|
+
}
|
|
445
463
|
}
|
|
446
464
|
}
|
|
447
465
|
|
|
@@ -868,15 +886,25 @@ class Request {
|
|
|
868
886
|
static setRequestState (request, newState) {
|
|
869
887
|
request.#state = newState
|
|
870
888
|
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Removes the `abort` listener that makes this request's signal follow the
|
|
892
|
+
* signal passed to its constructor, if any. Idempotent.
|
|
893
|
+
* @param {Request} request
|
|
894
|
+
*/
|
|
895
|
+
static removeRequestAbortListener (request) {
|
|
896
|
+
request.#abortCleanup?.()
|
|
897
|
+
}
|
|
871
898
|
}
|
|
872
899
|
|
|
873
|
-
const { setRequestSignal, getRequestDispatcher, setRequestDispatcher, setRequestHeaders, getRequestState, setRequestState } = Request
|
|
900
|
+
const { setRequestSignal, getRequestDispatcher, setRequestDispatcher, setRequestHeaders, getRequestState, setRequestState, removeRequestAbortListener } = Request
|
|
874
901
|
Reflect.deleteProperty(Request, 'setRequestSignal')
|
|
875
902
|
Reflect.deleteProperty(Request, 'getRequestDispatcher')
|
|
876
903
|
Reflect.deleteProperty(Request, 'setRequestDispatcher')
|
|
877
904
|
Reflect.deleteProperty(Request, 'setRequestHeaders')
|
|
878
905
|
Reflect.deleteProperty(Request, 'getRequestState')
|
|
879
906
|
Reflect.deleteProperty(Request, 'setRequestState')
|
|
907
|
+
Reflect.deleteProperty(Request, 'removeRequestAbortListener')
|
|
880
908
|
|
|
881
909
|
mixinBody(Request, getRequestState)
|
|
882
910
|
|
|
@@ -1111,5 +1139,6 @@ module.exports = {
|
|
|
1111
1139
|
fromInnerRequest,
|
|
1112
1140
|
cloneRequest,
|
|
1113
1141
|
getRequestDispatcher,
|
|
1114
|
-
getRequestState
|
|
1142
|
+
getRequestState,
|
|
1143
|
+
removeRequestAbortListener
|
|
1115
1144
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.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": {
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
"@sinonjs/fake-timers": "^12.0.0",
|
|
116
116
|
"@types/node": "^22.0.0",
|
|
117
117
|
"abort-controller": "^3.0.0",
|
|
118
|
-
"borp": "^0.
|
|
119
|
-
"c8": "^
|
|
118
|
+
"borp": "^1.0.0",
|
|
119
|
+
"c8": "^11.0.0",
|
|
120
120
|
"cross-env": "^10.0.0",
|
|
121
121
|
"dns-packet": "^5.4.0",
|
|
122
122
|
"esbuild": "^0.28.0",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"husky": "^9.0.7",
|
|
126
126
|
"jest": "^30.0.5",
|
|
127
127
|
"jsondiffpatch": "^0.7.3",
|
|
128
|
-
"neostandard": "^0.
|
|
128
|
+
"neostandard": "^0.13.0",
|
|
129
129
|
"node-forge": "^1.3.1",
|
|
130
130
|
"proxy": "^4.0.0",
|
|
131
131
|
"tsd": "^0.33.0",
|
package/types/client.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import Dispatcher from './dispatcher'
|
|
|
3
3
|
import buildConnector from './connector'
|
|
4
4
|
import TClientStats from './client-stats'
|
|
5
5
|
|
|
6
|
-
type ClientConnectOptions = Omit<Dispatcher.ConnectOptions
|
|
6
|
+
type ClientConnectOptions<TOpaque = null> = Omit<Dispatcher.ConnectOptions<TOpaque>, 'origin'>
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
|
|
@@ -20,12 +20,12 @@ export class Client extends Dispatcher {
|
|
|
20
20
|
readonly stats: TClientStats
|
|
21
21
|
|
|
22
22
|
// Override dispatcher APIs.
|
|
23
|
-
override connect (
|
|
24
|
-
options: ClientConnectOptions
|
|
25
|
-
): Promise<Dispatcher.ConnectData
|
|
26
|
-
override connect (
|
|
27
|
-
options: ClientConnectOptions
|
|
28
|
-
callback: (err: Error | null, data: Dispatcher.ConnectData) => void
|
|
23
|
+
override connect<TOpaque = null> (
|
|
24
|
+
options: ClientConnectOptions<TOpaque>
|
|
25
|
+
): Promise<Dispatcher.ConnectData<TOpaque>>
|
|
26
|
+
override connect<TOpaque = null> (
|
|
27
|
+
options: ClientConnectOptions<TOpaque>,
|
|
28
|
+
callback: (err: Error | null, data: Dispatcher.ConnectData<TOpaque>) => void
|
|
29
29
|
): void
|
|
30
30
|
}
|
|
31
31
|
|
package/types/connector.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ declare function buildConnector (options?: buildConnector.BuildOptions): buildCo
|
|
|
7
7
|
declare namespace buildConnector {
|
|
8
8
|
export type BuildOptions = (ConnectionOptions | TcpNetConnectOpts | IpcNetConnectOpts) & {
|
|
9
9
|
allowH2?: boolean;
|
|
10
|
+
preferH2?: boolean;
|
|
10
11
|
maxCachedSessions?: number | null;
|
|
11
12
|
socketPath?: string | null;
|
|
12
13
|
timeout?: number | null;
|
package/types/dispatcher.d.ts
CHANGED
|
@@ -121,8 +121,6 @@ declare namespace Dispatcher {
|
|
|
121
121
|
bodyTimeout?: number | null;
|
|
122
122
|
/** Whether the request should stablish a keep-alive or not. Default `false` */
|
|
123
123
|
reset?: boolean;
|
|
124
|
-
/** Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. Defaults to false */
|
|
125
|
-
throwOnError?: boolean;
|
|
126
124
|
/** For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server */
|
|
127
125
|
expectContinue?: boolean;
|
|
128
126
|
}
|
package/types/fetch.d.ts
CHANGED
|
@@ -36,7 +36,10 @@ export class BodyMixin {
|
|
|
36
36
|
readonly bytes: () => Promise<Uint8Array>
|
|
37
37
|
/**
|
|
38
38
|
* @deprecated This method is not recommended for parsing multipart/form-data bodies in server environments.
|
|
39
|
-
*
|
|
39
|
+
* Calling body.formData() buffers and parses the entire body. Since this is dictated by the spec,
|
|
40
|
+
* this method must only be called on responses from trusted servers.
|
|
41
|
+
* For responses from untrusted or user-controlled servers, use a dedicated streaming parser such as
|
|
42
|
+
* [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy) and apply application-specific limits as follows:
|
|
40
43
|
*
|
|
41
44
|
* @example
|
|
42
45
|
* ```js
|
package/types/formdata.d.ts
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
import { File } from 'node:buffer'
|
|
5
5
|
import { SpecIterableIterator } from './fetch'
|
|
6
6
|
|
|
7
|
-
declare module 'node:buffer' {
|
|
8
|
-
interface File {
|
|
9
|
-
readonly [Symbol.toStringTag]: string
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
/**
|
|
14
8
|
* A `string` or `File` that represents a single value from a set of `FormData` key-value pairs.
|
|
15
9
|
*/
|
package/types/interceptors.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export default Interceptors
|
|
|
8
8
|
declare namespace Interceptors {
|
|
9
9
|
export type DumpInterceptorOpts = { maxSize?: number }
|
|
10
10
|
export type RetryInterceptorOpts = RetryHandler.RetryOptions
|
|
11
|
-
export type RedirectInterceptorOpts = { maxRedirections?: number, throwOnMaxRedirect?: boolean }
|
|
11
|
+
export type RedirectInterceptorOpts = { maxRedirections?: number, throwOnMaxRedirect?: boolean, stripHeadersOnRedirect?: string[], stripHeadersOnCrossOriginRedirect?: string[] }
|
|
12
12
|
export type DecompressInterceptorOpts = {
|
|
13
13
|
skipErrorResponses?: boolean
|
|
14
14
|
skipStatusCodes?: number[]
|
|
@@ -30,7 +30,9 @@ declare namespace SnapshotRecorder {
|
|
|
30
30
|
ignoreHeaders?: string[]
|
|
31
31
|
excludeHeaders?: string[]
|
|
32
32
|
matchBody?: boolean
|
|
33
|
+
normalizeBody?: (body: string | Buffer | null | undefined) => string
|
|
33
34
|
matchQuery?: boolean
|
|
35
|
+
normalizeQuery?: (query: URLSearchParams) => string
|
|
34
36
|
caseSensitive?: boolean
|
|
35
37
|
shouldRecord?: (requestOpts: any) => boolean
|
|
36
38
|
shouldPlayback?: (requestOpts: any) => boolean
|
|
@@ -98,7 +100,9 @@ declare namespace SnapshotAgent {
|
|
|
98
100
|
ignoreHeaders?: string[]
|
|
99
101
|
excludeHeaders?: string[]
|
|
100
102
|
matchBody?: boolean
|
|
103
|
+
normalizeBody?: (body: string | Buffer | null | undefined) => string
|
|
101
104
|
matchQuery?: boolean
|
|
105
|
+
normalizeQuery?: (query: URLSearchParams) => string
|
|
102
106
|
caseSensitive?: boolean
|
|
103
107
|
shouldRecord?: (requestOpts: any) => boolean
|
|
104
108
|
shouldPlayback?: (requestOpts: any) => boolean
|