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.
@@ -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
- * **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.
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
@@ -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
- this[kInterceptors] = interceptors?.Client && Array.isArray(interceptors.Client)
211
- ? interceptors.Client
212
- : [createRedirectInterceptor({ maxRedirections })]
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
- const result = []
447
+ let start = position.position
448
448
 
449
- while (position.position < input.length && condition(input[position.position])) {
450
- result.push(input[position.position])
451
-
452
- position.position++
449
+ while (start < input.length && condition(input[start])) {
450
+ ++start
453
451
  }
454
452
 
455
- return Buffer.from(result, result.length)
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')
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.8.0",
3
+ "version": "6.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": {
package/types/fetch.d.ts CHANGED
@@ -122,7 +122,7 @@ export interface RequestInit {
122
122
  method?: string
123
123
  keepalive?: boolean
124
124
  headers?: HeadersInit
125
- body?: BodyInit
125
+ body?: BodyInit | null
126
126
  redirect?: RequestRedirect
127
127
  integrity?: string
128
128
  signal?: AbortSignal | null