undici 6.8.0 → 6.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/docs/api/Client.md +2 -1
- package/docs/docs/api/Dispatcher.md +135 -0
- package/index.js +4 -0
- package/lib/dispatcher/client.js +13 -3
- package/lib/dispatcher/dispatcher.js +47 -1
- package/lib/interceptor/redirect.js +24 -0
- package/lib/interceptor/retry.js +19 -0
- package/lib/web/fetch/formdata-parser.js +4 -6
- package/lib/web/fetch/formdata.js +10 -0
- package/lib/web/fetch/request.js +27 -0
- package/lib/web/fetch/response.js +21 -0
- package/package.json +1 -1
- package/types/fetch.d.ts +1 -1
package/docs/docs/api/Client.md
CHANGED
|
@@ -29,7 +29,8 @@ Returns: `Client`
|
|
|
29
29
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
|
30
30
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
31
31
|
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
|
|
32
|
-
|
|
32
|
+
<!-- TODO: Remove once we drop its support -->
|
|
33
|
+
* **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time. **Note: this is deprecated in favor of [Dispatcher#compose](./Dispatcher.md#dispatcher). Support will be droped in next major.**
|
|
33
34
|
* **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
|
|
34
35
|
* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
|
|
35
36
|
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
|
|
@@ -817,6 +817,141 @@ try {
|
|
|
817
817
|
}
|
|
818
818
|
```
|
|
819
819
|
|
|
820
|
+
### `Dispatcher.compose(interceptors[, interceptor])`
|
|
821
|
+
|
|
822
|
+
Compose a new dispatcher from the current dispatcher and the given interceptors.
|
|
823
|
+
|
|
824
|
+
> _Notes_:
|
|
825
|
+
> - The order of the interceptors matters. The first interceptor will be the first to be called.
|
|
826
|
+
> - It is important to note that the `interceptor` function should return a function that follows the `Dispatcher.dispatch` signature.
|
|
827
|
+
> - Any fork of the chain of `interceptors` can lead to unexpected results.
|
|
828
|
+
|
|
829
|
+
Arguments:
|
|
830
|
+
|
|
831
|
+
* **interceptors** `Interceptor[interceptor[]]`: It is an array of `Interceptor` functions passed as only argument, or several interceptors passed as separate arguments.
|
|
832
|
+
|
|
833
|
+
Returns: `Dispatcher`.
|
|
834
|
+
|
|
835
|
+
#### Parameter: `Interceptor`
|
|
836
|
+
|
|
837
|
+
A function that takes a `dispatch` method and returns a `dispatch`-like function.
|
|
838
|
+
|
|
839
|
+
#### Example 1 - Basic Compose
|
|
840
|
+
|
|
841
|
+
```js
|
|
842
|
+
const { Client, RedirectHandler } = require('undici')
|
|
843
|
+
|
|
844
|
+
const redirectInterceptor = dispatch => {
|
|
845
|
+
return (opts, handler) => {
|
|
846
|
+
const { maxRedirections } = opts
|
|
847
|
+
|
|
848
|
+
if (!maxRedirections) {
|
|
849
|
+
return dispatch(opts, handler)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const redirectHandler = new RedirectHandler(
|
|
853
|
+
dispatch,
|
|
854
|
+
maxRedirections,
|
|
855
|
+
opts,
|
|
856
|
+
handler
|
|
857
|
+
)
|
|
858
|
+
opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
|
|
859
|
+
return dispatch(opts, redirectHandler)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const client = new Client('http://localhost:3000')
|
|
864
|
+
.compose(redirectInterceptor)
|
|
865
|
+
|
|
866
|
+
await client.request({ path: '/', method: 'GET' })
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
#### Example 2 - Chained Compose
|
|
870
|
+
|
|
871
|
+
```js
|
|
872
|
+
const { Client, RedirectHandler, RetryHandler } = require('undici')
|
|
873
|
+
|
|
874
|
+
const redirectInterceptor = dispatch => {
|
|
875
|
+
return (opts, handler) => {
|
|
876
|
+
const { maxRedirections } = opts
|
|
877
|
+
|
|
878
|
+
if (!maxRedirections) {
|
|
879
|
+
return dispatch(opts, handler)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const redirectHandler = new RedirectHandler(
|
|
883
|
+
dispatch,
|
|
884
|
+
maxRedirections,
|
|
885
|
+
opts,
|
|
886
|
+
handler
|
|
887
|
+
)
|
|
888
|
+
opts = { ...opts, maxRedirections: 0 }
|
|
889
|
+
return dispatch(opts, redirectHandler)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const retryInterceptor = dispatch => {
|
|
894
|
+
return function retryInterceptor (opts, handler) {
|
|
895
|
+
return dispatch(
|
|
896
|
+
opts,
|
|
897
|
+
new RetryHandler(opts, {
|
|
898
|
+
handler,
|
|
899
|
+
dispatch
|
|
900
|
+
})
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const client = new Client('http://localhost:3000')
|
|
906
|
+
.compose(redirectInterceptor)
|
|
907
|
+
.compose(retryInterceptor)
|
|
908
|
+
|
|
909
|
+
await client.request({ path: '/', method: 'GET' })
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
#### Pre-built interceptors
|
|
913
|
+
|
|
914
|
+
##### `redirect`
|
|
915
|
+
|
|
916
|
+
The `redirect` interceptor allows you to customize the way your dispatcher handles redirects.
|
|
917
|
+
|
|
918
|
+
It accepts the same arguments as the [`RedirectHandler` constructor](./RedirectHandler.md).
|
|
919
|
+
|
|
920
|
+
**Example - Basic Redirect Interceptor**
|
|
921
|
+
|
|
922
|
+
```js
|
|
923
|
+
const { Client, interceptors } = require("undici");
|
|
924
|
+
const { redirect } = interceptors;
|
|
925
|
+
|
|
926
|
+
const client = new Client("http://example.com").compose(
|
|
927
|
+
redirect({ maxRedirections: 3, throwOnMaxRedirects: true })
|
|
928
|
+
);
|
|
929
|
+
client.request({ path: "/" })
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
##### `retry`
|
|
933
|
+
|
|
934
|
+
The `retry` interceptor allows you to customize the way your dispatcher handles retries.
|
|
935
|
+
|
|
936
|
+
It accepts the same arguments as the [`RetryHandler` constructor](./RetryHandler.md).
|
|
937
|
+
|
|
938
|
+
**Example - Basic Redirect Interceptor**
|
|
939
|
+
|
|
940
|
+
```js
|
|
941
|
+
const { Client, interceptors } = require("undici");
|
|
942
|
+
const { retry } = interceptors;
|
|
943
|
+
|
|
944
|
+
const client = new Client("http://example.com").compose(
|
|
945
|
+
retry({
|
|
946
|
+
maxRetries: 3,
|
|
947
|
+
minTimeout: 1000,
|
|
948
|
+
maxTimeout: 10000,
|
|
949
|
+
timeoutFactor: 2,
|
|
950
|
+
retryAfter: true,
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
```
|
|
954
|
+
|
|
820
955
|
## Instance Events
|
|
821
956
|
|
|
822
957
|
### Event: `'connect'`
|
package/index.js
CHANGED
|
@@ -36,6 +36,10 @@ module.exports.RetryHandler = RetryHandler
|
|
|
36
36
|
module.exports.DecoratorHandler = DecoratorHandler
|
|
37
37
|
module.exports.RedirectHandler = RedirectHandler
|
|
38
38
|
module.exports.createRedirectInterceptor = createRedirectInterceptor
|
|
39
|
+
module.exports.interceptors = {
|
|
40
|
+
redirect: require('./lib/interceptor/redirect'),
|
|
41
|
+
retry: require('./lib/interceptor/retry')
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
module.exports.buildConnector = buildConnector
|
|
41
45
|
module.exports.errors = errors
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -59,6 +59,7 @@ const {
|
|
|
59
59
|
} = require('../core/symbols.js')
|
|
60
60
|
const connectH1 = require('./client-h1.js')
|
|
61
61
|
const connectH2 = require('./client-h2.js')
|
|
62
|
+
let deprecatedInterceptorWarned = false
|
|
62
63
|
|
|
63
64
|
const kClosedResolve = Symbol('kClosedResolve')
|
|
64
65
|
|
|
@@ -207,9 +208,18 @@ class Client extends DispatcherBase {
|
|
|
207
208
|
})
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
if (interceptors?.Client && Array.isArray(interceptors.Client)) {
|
|
212
|
+
this[kInterceptors] = interceptors.Client
|
|
213
|
+
if (!deprecatedInterceptorWarned) {
|
|
214
|
+
deprecatedInterceptorWarned = true
|
|
215
|
+
process.emitWarning('Client.Options#interceptor is deprecated. Use Dispatcher#compose instead.', {
|
|
216
|
+
code: 'UNDICI-CLIENT-INTERCEPTOR-DEPRECATED'
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
this[kInterceptors] = [createRedirectInterceptor({ maxRedirections })]
|
|
221
|
+
}
|
|
222
|
+
|
|
213
223
|
this[kUrl] = util.parseOrigin(url)
|
|
214
224
|
this[kConnector] = connect
|
|
215
225
|
this[kPipelining] = pipelining != null ? pipelining : 1
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
-
|
|
3
2
|
const EventEmitter = require('node:events')
|
|
4
3
|
|
|
5
4
|
class Dispatcher extends EventEmitter {
|
|
@@ -14,6 +13,53 @@ class Dispatcher extends EventEmitter {
|
|
|
14
13
|
destroy () {
|
|
15
14
|
throw new Error('not implemented')
|
|
16
15
|
}
|
|
16
|
+
|
|
17
|
+
compose (...args) {
|
|
18
|
+
// So we handle [interceptor1, interceptor2] or interceptor1, interceptor2, ...
|
|
19
|
+
const interceptors = Array.isArray(args[0]) ? args[0] : args
|
|
20
|
+
let dispatch = this.dispatch.bind(this)
|
|
21
|
+
|
|
22
|
+
for (const interceptor of interceptors) {
|
|
23
|
+
if (interceptor == null) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof interceptor !== 'function') {
|
|
28
|
+
throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dispatch = interceptor(dispatch)
|
|
32
|
+
|
|
33
|
+
if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
|
|
34
|
+
throw new TypeError('invalid interceptor')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new ComposedDispatcher(this, dispatch)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ComposedDispatcher extends Dispatcher {
|
|
43
|
+
#dispatcher = null
|
|
44
|
+
#dispatch = null
|
|
45
|
+
|
|
46
|
+
constructor (dispatcher, dispatch) {
|
|
47
|
+
super()
|
|
48
|
+
this.#dispatcher = dispatcher
|
|
49
|
+
this.#dispatch = dispatch
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dispatch (...args) {
|
|
53
|
+
this.#dispatch(...args)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
close (...args) {
|
|
57
|
+
return this.#dispatcher.close(...args)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
destroy (...args) {
|
|
61
|
+
return this.#dispatcher.destroy(...args)
|
|
62
|
+
}
|
|
17
63
|
}
|
|
18
64
|
|
|
19
65
|
module.exports = Dispatcher
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const RedirectHandler = require('../handler/redirect-handler')
|
|
3
|
+
|
|
4
|
+
module.exports = opts => {
|
|
5
|
+
const globalMaxRedirections = opts?.maxRedirections
|
|
6
|
+
return dispatch => {
|
|
7
|
+
return function redirectInterceptor (opts, handler) {
|
|
8
|
+
const { maxRedirections = globalMaxRedirections, ...baseOpts } = opts
|
|
9
|
+
|
|
10
|
+
if (!maxRedirections) {
|
|
11
|
+
return dispatch(opts, handler)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const redirectHandler = new RedirectHandler(
|
|
15
|
+
dispatch,
|
|
16
|
+
maxRedirections,
|
|
17
|
+
opts,
|
|
18
|
+
handler
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return dispatch(baseOpts, redirectHandler)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const RetryHandler = require('../handler/retry-handler')
|
|
3
|
+
|
|
4
|
+
module.exports = globalOpts => {
|
|
5
|
+
return dispatch => {
|
|
6
|
+
return function retryInterceptor (opts, handler) {
|
|
7
|
+
return dispatch(
|
|
8
|
+
opts,
|
|
9
|
+
new RetryHandler(
|
|
10
|
+
{ ...opts, retryOptions: { ...globalOpts, ...opts.retryOptions } },
|
|
11
|
+
{
|
|
12
|
+
handler,
|
|
13
|
+
dispatch
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -444,15 +444,13 @@ function parseMultipartFormDataName (input, position) {
|
|
|
444
444
|
* @param {{ position: number }} position
|
|
445
445
|
*/
|
|
446
446
|
function collectASequenceOfBytes (condition, input, position) {
|
|
447
|
-
|
|
447
|
+
let start = position.position
|
|
448
448
|
|
|
449
|
-
while (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
position.position++
|
|
449
|
+
while (start < input.length && condition(input[start])) {
|
|
450
|
+
++start
|
|
453
451
|
}
|
|
454
452
|
|
|
455
|
-
return
|
|
453
|
+
return input.subarray(position.position, (position.position = start))
|
|
456
454
|
}
|
|
457
455
|
|
|
458
456
|
/**
|
|
@@ -6,6 +6,7 @@ const { kEnumerableProperty } = require('../../core/util')
|
|
|
6
6
|
const { File: UndiciFile, FileLike, isFileLike } = require('./file')
|
|
7
7
|
const { webidl } = require('./webidl')
|
|
8
8
|
const { File: NativeFile } = require('node:buffer')
|
|
9
|
+
const nodeUtil = require('node:util')
|
|
9
10
|
|
|
10
11
|
/** @type {globalThis['File']} */
|
|
11
12
|
const File = NativeFile ?? UndiciFile
|
|
@@ -154,6 +155,15 @@ class FormData {
|
|
|
154
155
|
this[kState].push(entry)
|
|
155
156
|
}
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
[nodeUtil.inspect.custom] (depth, options) {
|
|
160
|
+
let output = 'FormData:\n'
|
|
161
|
+
this[kState].forEach(entry => {
|
|
162
|
+
output += `${entry.name}: ${entry.value}\n`
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return output
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
iteratorMixin('FormData', FormData, kState, 'name', 'value')
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -6,6 +6,7 @@ const { extractBody, mixinBody, cloneBody } = require('./body')
|
|
|
6
6
|
const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
|
|
7
7
|
const { FinalizationRegistry } = require('./dispatcher-weakref')()
|
|
8
8
|
const util = require('../../core/util')
|
|
9
|
+
const nodeUtil = require('node:util')
|
|
9
10
|
const {
|
|
10
11
|
isValidHTTPToken,
|
|
11
12
|
sameOrigin,
|
|
@@ -771,6 +772,32 @@ class Request {
|
|
|
771
772
|
// 4. Return clonedRequestObject.
|
|
772
773
|
return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders][kGuard], this[kRealm])
|
|
773
774
|
}
|
|
775
|
+
|
|
776
|
+
[nodeUtil.inspect.custom] (depth, options) {
|
|
777
|
+
if (options.depth === null) {
|
|
778
|
+
options.depth = 2
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const properties = {
|
|
782
|
+
method: this.method,
|
|
783
|
+
url: this.url,
|
|
784
|
+
headers: this.headers,
|
|
785
|
+
destination: this.destination,
|
|
786
|
+
referrer: this.referrer,
|
|
787
|
+
referrerPolicy: this.referrerPolicy,
|
|
788
|
+
mode: this.mode,
|
|
789
|
+
credentials: this.credentials,
|
|
790
|
+
cache: this.cache,
|
|
791
|
+
redirect: this.redirect,
|
|
792
|
+
integrity: this.integrity,
|
|
793
|
+
keepalive: this.keepalive,
|
|
794
|
+
isReloadNavigation: this.isReloadNavigation,
|
|
795
|
+
isHistoryNavigation: this.isHistoryNavigation,
|
|
796
|
+
signal: this.signal
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return nodeUtil.formatWithOptions(options, { ...properties })
|
|
800
|
+
}
|
|
774
801
|
}
|
|
775
802
|
|
|
776
803
|
mixinBody(Request)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { Headers, HeadersList, fill } = require('./headers')
|
|
4
4
|
const { extractBody, cloneBody, mixinBody } = require('./body')
|
|
5
5
|
const util = require('../../core/util')
|
|
6
|
+
const nodeUtil = require('node:util')
|
|
6
7
|
const { kEnumerableProperty } = util
|
|
7
8
|
const {
|
|
8
9
|
isValidReasonPhrase,
|
|
@@ -252,6 +253,26 @@ class Response {
|
|
|
252
253
|
// clonedResponse, this’s headers’s guard, and this’s relevant Realm.
|
|
253
254
|
return fromInnerResponse(clonedResponse, this[kHeaders][kGuard], this[kRealm])
|
|
254
255
|
}
|
|
256
|
+
|
|
257
|
+
[nodeUtil.inspect.custom] (depth, options) {
|
|
258
|
+
if (options.depth === null) {
|
|
259
|
+
options.depth = 2
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const properties = {
|
|
263
|
+
status: this.status,
|
|
264
|
+
statusText: this.statusText,
|
|
265
|
+
headers: this.headers,
|
|
266
|
+
body: this.body,
|
|
267
|
+
bodyUsed: this.bodyUsed,
|
|
268
|
+
ok: this.ok,
|
|
269
|
+
redirected: this.redirected,
|
|
270
|
+
type: this.type,
|
|
271
|
+
url: this.url
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return nodeUtil.formatWithOptions(options, `Response ${nodeUtil.inspect(properties)}`)
|
|
275
|
+
}
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
mixinBody(Response)
|
package/package.json
CHANGED
package/types/fetch.d.ts
CHANGED