undici 7.0.0-alpha.2 → 7.0.0-alpha.4

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.
Files changed (56) hide show
  1. package/README.md +3 -2
  2. package/docs/docs/api/BalancedPool.md +1 -1
  3. package/docs/docs/api/CacheStore.md +100 -0
  4. package/docs/docs/api/Dispatcher.md +32 -2
  5. package/docs/docs/api/MockClient.md +1 -1
  6. package/docs/docs/api/Pool.md +1 -1
  7. package/docs/docs/api/api-lifecycle.md +2 -2
  8. package/docs/docs/best-practices/mocking-request.md +2 -2
  9. package/docs/docs/best-practices/proxy.md +1 -1
  10. package/index.d.ts +1 -1
  11. package/index.js +8 -2
  12. package/lib/api/api-request.js +2 -2
  13. package/lib/api/readable.js +6 -6
  14. package/lib/cache/memory-cache-store.js +325 -0
  15. package/lib/core/connect.js +5 -0
  16. package/lib/core/constants.js +24 -1
  17. package/lib/core/request.js +2 -2
  18. package/lib/core/util.js +13 -1
  19. package/lib/dispatcher/client-h1.js +100 -87
  20. package/lib/dispatcher/client-h2.js +168 -96
  21. package/lib/dispatcher/pool-base.js +3 -3
  22. package/lib/handler/cache-handler.js +389 -0
  23. package/lib/handler/cache-revalidation-handler.js +151 -0
  24. package/lib/handler/redirect-handler.js +5 -3
  25. package/lib/handler/retry-handler.js +3 -3
  26. package/lib/interceptor/cache.js +192 -0
  27. package/lib/interceptor/dns.js +71 -48
  28. package/lib/util/cache.js +249 -0
  29. package/lib/web/cache/cache.js +1 -0
  30. package/lib/web/cache/cachestorage.js +2 -0
  31. package/lib/web/cookies/index.js +12 -1
  32. package/lib/web/cookies/parse.js +6 -1
  33. package/lib/web/eventsource/eventsource.js +2 -0
  34. package/lib/web/fetch/body.js +1 -5
  35. package/lib/web/fetch/constants.js +12 -5
  36. package/lib/web/fetch/data-url.js +2 -2
  37. package/lib/web/fetch/formdata-parser.js +70 -43
  38. package/lib/web/fetch/formdata.js +3 -1
  39. package/lib/web/fetch/headers.js +3 -1
  40. package/lib/web/fetch/index.js +4 -6
  41. package/lib/web/fetch/request.js +3 -1
  42. package/lib/web/fetch/response.js +3 -1
  43. package/lib/web/fetch/util.js +171 -47
  44. package/lib/web/fetch/webidl.js +28 -16
  45. package/lib/web/websocket/constants.js +67 -6
  46. package/lib/web/websocket/events.js +4 -0
  47. package/lib/web/websocket/stream/websocketerror.js +1 -1
  48. package/lib/web/websocket/websocket.js +2 -0
  49. package/package.json +8 -5
  50. package/types/cache-interceptor.d.ts +101 -0
  51. package/types/cookies.d.ts +2 -0
  52. package/types/dispatcher.d.ts +1 -1
  53. package/types/fetch.d.ts +9 -8
  54. package/types/index.d.ts +3 -1
  55. package/types/interceptors.d.ts +4 -1
  56. package/types/webidl.d.ts +7 -1
package/README.md CHANGED
@@ -132,7 +132,7 @@ Returns a promise with the result of the `Dispatcher.request` method.
132
132
 
133
133
  Calls `options.dispatcher.request(options)`.
134
134
 
135
- See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
135
+ See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./docs/examples/README.md) for examples.
136
136
 
137
137
  ### `undici.stream([url, options, ]factory): Promise`
138
138
 
@@ -402,7 +402,8 @@ Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
402
402
  ### Pipelining
403
403
 
404
404
  Undici will only use pipelining if configured with a `pipelining` factor
405
- greater than `1`.
405
+ greater than `1`. Also it is important to pass `blocking: false` to the
406
+ request options to properly pipeline requests.
406
407
 
407
408
  Undici always assumes that connections are persistent and will immediately
408
409
  pipeline requests, without checking whether the connection is persistent.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Extends: `undici.Dispatcher`
4
4
 
5
- A pool of [Pool](Pool.md) instances connected to multiple upstreams.
5
+ A pool of [Pool](/docs/docs/api/Pool.md) instances connected to multiple upstreams.
6
6
 
7
7
  Requests are not guaranteed to be dispatched in order of invocation.
8
8
 
@@ -0,0 +1,100 @@
1
+ # Cache Store
2
+
3
+ A Cache Store is responsible for storing and retrieving cached responses.
4
+ It is also responsible for deciding which specific response to use based off of
5
+ a response's `Vary` header (if present). It is expected to be compliant with
6
+ [RFC-9111](https://www.rfc-editor.org/rfc/rfc9111.html).
7
+
8
+ ## Pre-built Cache Stores
9
+
10
+ ### `MemoryCacheStore`
11
+
12
+ The `MemoryCacheStore` stores the responses in-memory.
13
+
14
+ **Options**
15
+
16
+ - `maxEntries` - The maximum amount of responses to store. Default `Infinity`.
17
+ - `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.
18
+
19
+ ## Defining a Custom Cache Store
20
+
21
+ The store must implement the following functions:
22
+
23
+ ### Getter: `isFull`
24
+
25
+ Optional. This tells the cache interceptor if the store is full or not. If this is true,
26
+ the cache interceptor will not attempt to cache the response.
27
+
28
+ ### Function: `get`
29
+
30
+ Parameters:
31
+
32
+ * **req** `Dispatcher.RequestOptions` - Incoming request
33
+
34
+ Returns: `GetResult | Promise<GetResult | undefined> | undefined` - If the request is cached, the cached response is returned. If the request's method is anything other than HEAD, the response is also returned.
35
+ If the request isn't cached, `undefined` is returned.
36
+
37
+ Response properties:
38
+
39
+ * **response** `CachedResponse` - The cached response data.
40
+ * **body** `Readable | undefined` - The response's body.
41
+
42
+ ### Function: `createWriteStream`
43
+
44
+ Parameters:
45
+
46
+ * **req** `Dispatcher.RequestOptions` - Incoming request
47
+ * **value** `CachedResponse` - Response to store
48
+
49
+ Returns: `Writable | undefined` - If the store is full, return `undefined`. Otherwise, return a writable so that the cache interceptor can stream the body and trailers to the store.
50
+
51
+ ## `CachedResponse`
52
+
53
+ This is an interface containing the majority of a response's data (minus the body).
54
+
55
+ ### Property `statusCode`
56
+
57
+ `number` - The response's HTTP status code.
58
+
59
+ ### Property `statusMessage`
60
+
61
+ `string` - The response's HTTP status message.
62
+
63
+ ### Property `rawHeaders`
64
+
65
+ `Buffer[]` - The response's headers.
66
+
67
+ ### Property `vary`
68
+
69
+ `Record<string, string | string[]> | undefined` - The headers defined by the response's `Vary` header
70
+ and their respective values for later comparison
71
+
72
+ For example, for a response like
73
+ ```
74
+ Vary: content-encoding, accepts
75
+ content-encoding: utf8
76
+ accepts: application/json
77
+ ```
78
+
79
+ This would be
80
+ ```js
81
+ {
82
+ 'content-encoding': 'utf8',
83
+ accepts: 'application/json'
84
+ }
85
+ ```
86
+
87
+ ### Property `cachedAt`
88
+
89
+ `number` - Time in millis that this value was cached.
90
+
91
+ ### Property `staleAt`
92
+
93
+ `number` - Time in millis that this value is considered stale.
94
+
95
+ ### Property `deleteAt`
96
+
97
+ `number` - Time in millis that this value is to be deleted from the cache. This
98
+ is either the same sa staleAt or the `max-stale` caching directive.
99
+
100
+ The store must not return a response after the time defined in this property.
@@ -197,7 +197,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
197
197
  * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
198
198
  * **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
199
199
  * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
200
- * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
200
+ * **blocking** `boolean` (optional) - Default: `method !== 'HEAD'` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
201
201
  * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
202
202
  * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
203
203
  * **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
@@ -527,6 +527,7 @@ try {
527
527
  console.log('headers', headers)
528
528
  body.setEncoding('utf8')
529
529
  body.on('data', console.log)
530
+ body.on('error', console.error)
530
531
  body.on('end', () => {
531
532
  console.log('trailers', trailers)
532
533
  })
@@ -630,6 +631,25 @@ try {
630
631
  }
631
632
  ```
632
633
 
634
+ #### Example 3 - Conditionally reading the body
635
+
636
+ Remember to fully consume the body even in the case when it is not read.
637
+
638
+ ```js
639
+ const { body, statusCode } = await client.request({
640
+ path: '/',
641
+ method: 'GET'
642
+ })
643
+
644
+ if (statusCode === 200) {
645
+ return await body.arrayBuffer()
646
+ }
647
+
648
+ await body.dump()
649
+
650
+ return null
651
+ ```
652
+
633
653
  ### `Dispatcher.stream(options, factory[, callback])`
634
654
 
635
655
  A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream.
@@ -1001,7 +1021,7 @@ The `dns` interceptor enables you to cache DNS lookups for a given duration, per
1001
1021
  - It can be either `'4` or `6`.
1002
1022
  - It will only take effect if `dualStack` is `false`.
1003
1023
  - `lookup: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void` - Custom lookup function. Default: `dns.lookup`.
1004
- - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback).
1024
+ - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback).
1005
1025
  - `pick: (origin: URL, records: DNSInterceptorRecords, affinity: 4 | 6) => DNSInterceptorRecord` - Custom pick function. Default: `RoundRobin`.
1006
1026
  - The function should return a single record from the records array.
1007
1027
  - By default a simplified version of Round Robin is used.
@@ -1233,6 +1253,16 @@ test('should not error if request status code is not in the specified error code
1233
1253
 
1234
1254
  The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
1235
1255
 
1256
+ ##### `Cache Interceptor`
1257
+
1258
+ The `cache` interceptor implements client-side response caching as described in
1259
+ [RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html).
1260
+
1261
+ **Options**
1262
+
1263
+ - `store` - The [`CacheStore`](./CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](./CacheStore.md#memorycachestore).
1264
+ - `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
1265
+
1236
1266
  ## Instance Events
1237
1267
 
1238
1268
  ### Event: `'connect'`
@@ -2,7 +2,7 @@
2
2
 
3
3
  Extends: `undici.Client`
4
4
 
5
- A mock client class that implements the same api as [MockPool](MockPool.md).
5
+ A mock client class that implements the same api as [MockPool](/docs/docs/api/MockPool.md).
6
6
 
7
7
  ## `new MockClient(origin, [options])`
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Extends: `undici.Dispatcher`
4
4
 
5
- A pool of [Client](Client.md) instances connected to the same upstream target.
5
+ A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target.
6
6
 
7
7
  Requests are not guaranteed to be dispatched in order of invocation.
8
8
 
@@ -1,6 +1,6 @@
1
1
  # Client Lifecycle
2
2
 
3
- An Undici [Client](Client.md) can be best described as a state machine. The following list is a summary of the various state transitions the `Client` will go through in its lifecycle. This document also contains detailed breakdowns of each state.
3
+ An Undici [Client](/docs/docs/api/Client.md) can be best described as a state machine. The following list is a summary of the various state transitions the `Client` will go through in its lifecycle. This document also contains detailed breakdowns of each state.
4
4
 
5
5
  > This diagram is not a perfect representation of the undici Client. Since the Client class is not actually implemented as a state-machine, actual execution may deviate slightly from what is described below. Consider this as a general resource for understanding the inner workings of the Undici client rather than some kind of formal specification.
6
6
 
@@ -28,7 +28,7 @@ stateDiagram-v2
28
28
  [*] --> idle
29
29
  idle --> pending : connect
30
30
  idle --> destroyed : destroy/close
31
-
31
+
32
32
  pending --> idle : timeout
33
33
  pending --> destroyed : destroy
34
34
 
@@ -1,6 +1,6 @@
1
1
  # Mocking Request
2
2
 
3
- Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
3
+ Undici has its own mocking [utility](/docs/docs/api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
4
4
 
5
5
  Example:
6
6
 
@@ -73,7 +73,7 @@ const badRequest = await bankTransfer('1234567890', '100')
73
73
  assert.deepEqual(badRequest, { message: 'bank account not found' })
74
74
  ```
75
75
 
76
- Explore other MockAgent functionality [here](../api/MockAgent.md)
76
+ Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
77
77
 
78
78
  ## Debug Mock Value
79
79
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Connecting through a proxy is possible by:
4
4
 
5
- - Using [ProxyAgent](../api/ProxyAgent.md).
5
+ - Using [ProxyAgent](/docs/docs/api/ProxyAgent.md).
6
6
  - Configuring `Client` or `Pool` constructor.
7
7
 
8
8
  The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './types/index'
2
1
  import Undici from './types/index'
3
2
  export default Undici
3
+ export * from './types/index'
package/index.js CHANGED
@@ -40,7 +40,12 @@ module.exports.interceptors = {
40
40
  redirect: require('./lib/interceptor/redirect'),
41
41
  retry: require('./lib/interceptor/retry'),
42
42
  dump: require('./lib/interceptor/dump'),
43
- dns: require('./lib/interceptor/dns')
43
+ dns: require('./lib/interceptor/dns'),
44
+ cache: require('./lib/interceptor/cache')
45
+ }
46
+
47
+ module.exports.cacheStores = {
48
+ MemoryCacheStore: require('./lib/cache/memory-cache-store')
44
49
  }
45
50
 
46
51
  module.exports.buildConnector = buildConnector
@@ -131,12 +136,13 @@ const { kConstruct } = require('./lib/core/symbols')
131
136
  // in an older version of Node, it doesn't have any use without fetch.
132
137
  module.exports.caches = new CacheStorage(kConstruct)
133
138
 
134
- const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/web/cookies')
139
+ const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = require('./lib/web/cookies')
135
140
 
136
141
  module.exports.deleteCookie = deleteCookie
137
142
  module.exports.getCookies = getCookies
138
143
  module.exports.getSetCookies = getSetCookies
139
144
  module.exports.setCookie = setCookie
145
+ module.exports.parseCookie = parseCookie
140
146
 
141
147
  const { parseMIMEType, serializeAMimeType } = require('./lib/web/fetch/data-url')
142
148
 
@@ -65,7 +65,7 @@ class RequestHandler extends AsyncResource {
65
65
  this.removeAbortListener = util.addAbortListener(signal, () => {
66
66
  this.reason = signal.reason ?? new RequestAbortedError()
67
67
  if (this.res) {
68
- util.destroy(this.res, this.reason)
68
+ util.destroy(this.res.on('error', noop), this.reason)
69
69
  } else if (this.abort) {
70
70
  this.abort(this.reason)
71
71
  }
@@ -153,7 +153,7 @@ class RequestHandler extends AsyncResource {
153
153
  this.res = null
154
154
  // Ensure all queued handlers are invoked before destroying res.
155
155
  queueMicrotask(() => {
156
- util.destroy(res, err)
156
+ util.destroy(res.on('error', noop), err)
157
157
  })
158
158
  }
159
159
 
@@ -164,7 +164,7 @@ class BodyReadable extends Readable {
164
164
  * @see https://fetch.spec.whatwg.org/#dom-body-text
165
165
  * @returns {Promise<string>}
166
166
  */
167
- async text () {
167
+ text () {
168
168
  return consume(this, 'text')
169
169
  }
170
170
 
@@ -174,7 +174,7 @@ class BodyReadable extends Readable {
174
174
  * @see https://fetch.spec.whatwg.org/#dom-body-json
175
175
  * @returns {Promise<unknown>}
176
176
  */
177
- async json () {
177
+ json () {
178
178
  return consume(this, 'json')
179
179
  }
180
180
 
@@ -184,7 +184,7 @@ class BodyReadable extends Readable {
184
184
  * @see https://fetch.spec.whatwg.org/#dom-body-blob
185
185
  * @returns {Promise<Blob>}
186
186
  */
187
- async blob () {
187
+ blob () {
188
188
  return consume(this, 'blob')
189
189
  }
190
190
 
@@ -194,7 +194,7 @@ class BodyReadable extends Readable {
194
194
  * @see https://fetch.spec.whatwg.org/#dom-body-bytes
195
195
  * @returns {Promise<Uint8Array>}
196
196
  */
197
- async bytes () {
197
+ bytes () {
198
198
  return consume(this, 'bytes')
199
199
  }
200
200
 
@@ -204,7 +204,7 @@ class BodyReadable extends Readable {
204
204
  * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
205
205
  * @returns {Promise<ArrayBuffer>}
206
206
  */
207
- async arrayBuffer () {
207
+ arrayBuffer () {
208
208
  return consume(this, 'arrayBuffer')
209
209
  }
210
210
 
@@ -355,7 +355,7 @@ function isUnusable (bodyReadable) {
355
355
  * @param {string} type
356
356
  * @returns {Promise<any>}
357
357
  */
358
- async function consume (stream, type) {
358
+ function consume (stream, type) {
359
359
  assert(!stream[kConsume])
360
360
 
361
361
  return new Promise((resolve, reject) => {