undici 6.3.0 → 6.5.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 CHANGED
@@ -18,34 +18,23 @@ npm i undici
18
18
  ## Benchmarks
19
19
 
20
20
  The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a
21
- number of unix sockets (connections) with a pipelining depth of 10 running on Node 20.6.0.
22
-
23
- ### Connections 1
24
-
25
-
26
- | Tests | Samples | Result | Tolerance | Difference with slowest |
27
- |---------------------|---------|---------------|-----------|-------------------------|
28
- | http - no keepalive | 15 | 5.32 req/sec | ± 2.61 % | - |
29
- | http - keepalive | 10 | 5.35 req/sec | ± 2.47 % | + 0.44 % |
30
- | undici - fetch | 15 | 41.85 req/sec | ± 2.49 % | + 686.04 % |
31
- | undici - pipeline | 40 | 50.36 req/sec | ± 2.77 % | + 845.92 % |
32
- | undici - stream | 15 | 60.58 req/sec | ± 2.75 % | + 1037.72 % |
33
- | undici - request | 10 | 61.19 req/sec | ± 2.60 % | + 1049.24 % |
34
- | undici - dispatch | 20 | 64.84 req/sec | ± 2.81 % | + 1117.81 % |
35
-
36
-
37
- ### Connections 50
38
-
39
- | Tests | Samples | Result | Tolerance | Difference with slowest |
40
- |---------------------|---------|------------------|-----------|-------------------------|
41
- | undici - fetch | 30 | 2107.19 req/sec | ± 2.69 % | - |
42
- | http - no keepalive | 10 | 2698.90 req/sec | ± 2.68 % | + 28.08 % |
43
- | http - keepalive | 10 | 4639.49 req/sec | ± 2.55 % | + 120.17 % |
44
- | undici - pipeline | 40 | 6123.33 req/sec | ± 2.97 % | + 190.59 % |
45
- | undici - stream | 50 | 9426.51 req/sec | ± 2.92 % | + 347.35 % |
46
- | undici - request | 10 | 10162.88 req/sec | ± 2.13 % | + 382.29 % |
47
- | undici - dispatch | 50 | 11191.11 req/sec | ± 2.98 % | + 431.09 % |
21
+ 50 TCP connections with a pipelining depth of 10 running on Node 20.10.0.
48
22
 
23
+ ```
24
+ │ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │
25
+ |─────────────────────|─────────|─────────────────|───────────|─────────────────────────|
26
+ │ got │ 45 │ 1661.71 req/sec │ ± 2.93 % │ - │
27
+ │ node-fetch │ 20 │ 2164.81 req/sec │ ± 2.63 % │ + 30.28 % │
28
+ │ undici - fetch │ 35 │ 2274.27 req/sec │ ± 2.70 % │ + 36.86 % │
29
+ │ http - no keepalive │ 15 │ 2376.04 req/sec │ ± 2.99 % │ + 42.99 % │
30
+ │ axios │ 25 │ 2612.93 req/sec │ ± 2.89 % │ + 57.24 % │
31
+ │ request │ 40 │ 2712.19 req/sec │ ± 2.92 % │ + 63.22 % │
32
+ │ http - keepalive │ 45 │ 4393.25 req/sec │ ± 2.86 % │ + 164.38 % │
33
+ │ undici - pipeline │ 45 │ 5484.69 req/sec │ ± 2.87 % │ + 230.06 % │
34
+ │ undici - request │ 55 │ 7773.98 req/sec │ ± 2.93 % │ + 367.83 % │
35
+ │ undici - stream │ 70 │ 8425.96 req/sec │ ± 2.91 % │ + 407.07 % │
36
+ │ undici - dispatch │ 50 │ 9488.99 req/sec │ ± 2.85 % │ + 471.04 % │
37
+ ```
49
38
 
50
39
  ## Quick Start
51
40
 
@@ -62,9 +51,7 @@ const {
62
51
  console.log('response received', statusCode)
63
52
  console.log('headers', headers)
64
53
 
65
- for await (const data of body) {
66
- console.log('data', data)
67
- }
54
+ for await (const data of body) { console.log('data', data) }
68
55
 
69
56
  console.log('trailers', trailers)
70
57
  ```
package/docs/api/Debug.md CHANGED
@@ -53,10 +53,10 @@ This flag enables debug statements for the `Websocket` API.
53
53
  > **Note**: statements can overlap with `UNDICI` ones if `undici` or `fetch` flag has been enabled as well.
54
54
 
55
55
  ```sh
56
- NODE_DEBUG=fetch node script.js
56
+ NODE_DEBUG=websocket node script.js
57
57
 
58
58
  WEBSOCKET 18309: connecting to echo.websocket.org using https:h1
59
59
  WEBSOCKET 18309: connected to echo.websocket.org using https:h1
60
60
  WEBSOCKET 18309: sending request to GET https://echo.websocket.org//
61
61
  WEBSOCKET 18309: connection opened <ip_address>
62
- ```
62
+ ```
@@ -0,0 +1,21 @@
1
+ # EventSource
2
+
3
+ Undici exposes a WHATWG spec-compliant implementation of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
4
+ for [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).
5
+
6
+ ## Instantiating EventSource
7
+
8
+ Undici exports a EventSource class. You can instantiate the EventSource as
9
+ follows:
10
+
11
+ ```mjs
12
+ import { EventSource } from 'undici'
13
+
14
+ const evenSource = new EventSource('http://localhost:3000')
15
+ evenSource.onmessage = (event) => {
16
+ console.log(event.data)
17
+ }
18
+ ```
19
+
20
+ More information about the EventSource API can be found on
21
+ [MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource).
@@ -0,0 +1,96 @@
1
+ # Class: RedirectHandler
2
+
3
+ A class that handles redirection logic for HTTP requests.
4
+
5
+ ## `new RedirectHandler(dispatch, maxRedirections, opts, handler, redirectionLimitReached)`
6
+
7
+ Arguments:
8
+
9
+ - **dispatch** `function` - The dispatch function to be called after every retry.
10
+ - **maxRedirections** `number` - Maximum number of redirections allowed.
11
+ - **opts** `object` - Options for handling redirection.
12
+ - **handler** `object` - An object containing handlers for different stages of the request lifecycle.
13
+ - **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior.
14
+
15
+ Returns: `RedirectHandler`
16
+
17
+ ### Parameters
18
+
19
+ - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection.
20
+ - **maxRedirections** `number` (required) - Maximum number of redirections allowed.
21
+ - **opts** `object` (required) - Options for handling redirection.
22
+ - **handler** `object` (required) - Handlers for different stages of the request lifecycle.
23
+ - **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior.
24
+
25
+ ### Properties
26
+
27
+ - **location** `string` - The current redirection location.
28
+ - **abort** `function` - The abort function.
29
+ - **opts** `object` - The options for handling redirection.
30
+ - **maxRedirections** `number` - Maximum number of redirections allowed.
31
+ - **handler** `object` - Handlers for different stages of the request lifecycle.
32
+ - **history** `Array` - An array representing the history of URLs during redirection.
33
+ - **redirectionLimitReached** `boolean` - Indicates whether the redirection limit has been reached.
34
+
35
+ ### Methods
36
+
37
+ #### `onConnect(abort)`
38
+
39
+ Called when the connection is established.
40
+
41
+ Parameters:
42
+
43
+ - **abort** `function` - The abort function.
44
+
45
+ #### `onUpgrade(statusCode, headers, socket)`
46
+
47
+ Called when an upgrade is requested.
48
+
49
+ Parameters:
50
+
51
+ - **statusCode** `number` - The HTTP status code.
52
+ - **headers** `object` - The headers received in the response.
53
+ - **socket** `object` - The socket object.
54
+
55
+ #### `onError(error)`
56
+
57
+ Called when an error occurs.
58
+
59
+ Parameters:
60
+
61
+ - **error** `Error` - The error that occurred.
62
+
63
+ #### `onHeaders(statusCode, headers, resume, statusText)`
64
+
65
+ Called when headers are received.
66
+
67
+ Parameters:
68
+
69
+ - **statusCode** `number` - The HTTP status code.
70
+ - **headers** `object` - The headers received in the response.
71
+ - **resume** `function` - The resume function.
72
+ - **statusText** `string` - The status text.
73
+
74
+ #### `onData(chunk)`
75
+
76
+ Called when data is received.
77
+
78
+ Parameters:
79
+
80
+ - **chunk** `Buffer` - The data chunk received.
81
+
82
+ #### `onComplete(trailers)`
83
+
84
+ Called when the request is complete.
85
+
86
+ Parameters:
87
+
88
+ - **trailers** `object` - The trailers received.
89
+
90
+ #### `onBodySent(chunk)`
91
+
92
+ Called when the request body is sent.
93
+
94
+ Parameters:
95
+
96
+ - **chunk** `Buffer` - The chunk of the request body sent.
package/index.js CHANGED
@@ -21,14 +21,6 @@ const DecoratorHandler = require('./lib/handler/DecoratorHandler')
21
21
  const RedirectHandler = require('./lib/handler/RedirectHandler')
22
22
  const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
23
23
 
24
- let hasCrypto
25
- try {
26
- require('crypto')
27
- hasCrypto = true
28
- } catch {
29
- hasCrypto = false
30
- }
31
-
32
24
  Object.assign(Dispatcher.prototype, api)
33
25
 
34
26
  module.exports.Dispatcher = Dispatcher
@@ -102,14 +94,10 @@ function makeDispatcher (fn) {
102
94
  module.exports.setGlobalDispatcher = setGlobalDispatcher
103
95
  module.exports.getGlobalDispatcher = getGlobalDispatcher
104
96
 
105
- let fetchImpl = null
106
- module.exports.fetch = async function fetch (resource) {
107
- if (!fetchImpl) {
108
- fetchImpl = require('./lib/fetch').fetch
109
- }
110
-
97
+ const fetchImpl = require('./lib/fetch').fetch
98
+ module.exports.fetch = async function fetch (init, options = undefined) {
111
99
  try {
112
- return await fetchImpl(...arguments)
100
+ return await fetchImpl(init, options)
113
101
  } catch (err) {
114
102
  if (typeof err === 'object') {
115
103
  Error.captureStackTrace(err, this)
@@ -149,11 +137,7 @@ const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL')
149
137
  module.exports.parseMIMEType = parseMIMEType
150
138
  module.exports.serializeAMimeType = serializeAMimeType
151
139
 
152
- if (hasCrypto) {
153
- const { WebSocket } = require('./lib/websocket/websocket')
154
-
155
- module.exports.WebSocket = WebSocket
156
- }
140
+ module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket
157
141
 
158
142
  module.exports.request = makeDispatcher(api.request)
159
143
  module.exports.stream = makeDispatcher(api.stream)
@@ -165,3 +149,7 @@ module.exports.MockClient = MockClient
165
149
  module.exports.MockPool = MockPool
166
150
  module.exports.MockAgent = MockAgent
167
151
  module.exports.mockErrors = mockErrors
152
+
153
+ const { EventSource } = require('./lib/eventsource/eventsource')
154
+
155
+ module.exports.EventSource = EventSource
@@ -94,7 +94,7 @@ module.exports = class BodyReadable extends Readable {
94
94
  }
95
95
 
96
96
  push (chunk) {
97
- if (this[kConsume] && chunk !== null && this.readableLength === 0) {
97
+ if (this[kConsume] && chunk !== null) {
98
98
  consumePush(this[kConsume], chunk)
99
99
  return this[kReading] ? super.push(chunk) : true
100
100
  }
@@ -215,26 +215,28 @@ async function consume (stream, type) {
215
215
  reject(rState.errored ?? new TypeError('unusable'))
216
216
  }
217
217
  } else {
218
- stream[kConsume] = {
219
- type,
220
- stream,
221
- resolve,
222
- reject,
223
- length: 0,
224
- body: []
225
- }
218
+ queueMicrotask(() => {
219
+ stream[kConsume] = {
220
+ type,
221
+ stream,
222
+ resolve,
223
+ reject,
224
+ length: 0,
225
+ body: []
226
+ }
226
227
 
227
- stream
228
- .on('error', function (err) {
229
- consumeFinish(this[kConsume], err)
230
- })
231
- .on('close', function () {
232
- if (this[kConsume].body !== null) {
233
- consumeFinish(this[kConsume], new RequestAbortedError())
234
- }
235
- })
228
+ stream
229
+ .on('error', function (err) {
230
+ consumeFinish(this[kConsume], err)
231
+ })
232
+ .on('close', function () {
233
+ if (this[kConsume].body !== null) {
234
+ consumeFinish(this[kConsume], new RequestAbortedError())
235
+ }
236
+ })
236
237
 
237
- queueMicrotask(() => consumeStart(stream[kConsume]))
238
+ consumeStart(stream[kConsume])
239
+ })
238
240
  }
239
241
  })
240
242
  }
@@ -246,8 +248,16 @@ function consumeStart (consume) {
246
248
 
247
249
  const { _readableState: state } = consume.stream
248
250
 
249
- for (const chunk of state.buffer) {
250
- consumePush(consume, chunk)
251
+ if (state.bufferIndex) {
252
+ const start = state.bufferIndex
253
+ const end = state.buffer.length
254
+ for (let n = start; n < end; n++) {
255
+ consumePush(consume, state.buffer[n])
256
+ }
257
+ } else {
258
+ for (const chunk of state.buffer) {
259
+ consumePush(consume, chunk)
260
+ }
251
261
  }
252
262
 
253
263
  if (state.endEmitted) {
@@ -6,8 +6,9 @@ const { kEnumerableProperty, isDisturbed } = require('../core/util')
6
6
  const { kHeadersList } = require('../core/symbols')
7
7
  const { webidl } = require('../fetch/webidl')
8
8
  const { Response, cloneResponse } = require('../fetch/response')
9
- const { Request } = require('../fetch/request')
10
- const { kState, kHeaders, kGuard, kRealm } = require('../fetch/symbols')
9
+ const { Request, fromInnerRequest } = require('../fetch/request')
10
+ const { Headers } = require('../fetch/headers')
11
+ const { kState, kHeaders, kGuard } = require('../fetch/symbols')
11
12
  const { fetching } = require('../fetch/index')
12
13
  const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util')
13
14
  const assert = require('assert')
@@ -49,7 +50,7 @@ class Cache {
49
50
  request = webidl.converters.RequestInfo(request)
50
51
  options = webidl.converters.CacheQueryOptions(options)
51
52
 
52
- const p = await this.matchAll(request, options)
53
+ const p = this.#internalMatchAll(request, options, 1)
53
54
 
54
55
  if (p.length === 0) {
55
56
  return
@@ -64,64 +65,7 @@ class Cache {
64
65
  if (request !== undefined) request = webidl.converters.RequestInfo(request)
65
66
  options = webidl.converters.CacheQueryOptions(options)
66
67
 
67
- // 1.
68
- let r = null
69
-
70
- // 2.
71
- if (request !== undefined) {
72
- if (request instanceof Request) {
73
- // 2.1.1
74
- r = request[kState]
75
-
76
- // 2.1.2
77
- if (r.method !== 'GET' && !options.ignoreMethod) {
78
- return []
79
- }
80
- } else if (typeof request === 'string') {
81
- // 2.2.1
82
- r = new Request(request)[kState]
83
- }
84
- }
85
-
86
- // 5.
87
- // 5.1
88
- const responses = []
89
-
90
- // 5.2
91
- if (request === undefined) {
92
- // 5.2.1
93
- for (const requestResponse of this.#relevantRequestResponseList) {
94
- responses.push(requestResponse[1])
95
- }
96
- } else { // 5.3
97
- // 5.3.1
98
- const requestResponses = this.#queryCache(r, options)
99
-
100
- // 5.3.2
101
- for (const requestResponse of requestResponses) {
102
- responses.push(requestResponse[1])
103
- }
104
- }
105
-
106
- // 5.4
107
- // We don't implement CORs so we don't need to loop over the responses, yay!
108
-
109
- // 5.5.1
110
- const responseList = []
111
-
112
- // 5.5.2
113
- for (const response of responses) {
114
- // 5.5.2.1
115
- const responseObject = new Response(null)
116
- responseObject[kState] = response
117
- responseObject[kHeaders][kHeadersList] = response.headersList
118
- responseObject[kHeaders][kGuard] = 'immutable'
119
-
120
- responseList.push(responseObject.clone())
121
- }
122
-
123
- // 6.
124
- return Object.freeze(responseList)
68
+ return this.#internalMatchAll(request, options)
125
69
  }
126
70
 
127
71
  async add (request) {
@@ -500,7 +444,7 @@ class Cache {
500
444
  * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
501
445
  * @param {any} request
502
446
  * @param {import('../../types/cache').CacheQueryOptions} options
503
- * @returns {readonly Request[]}
447
+ * @returns {Promise<readonly Request[]>}
504
448
  */
505
449
  async keys (request = undefined, options = {}) {
506
450
  webidl.brandCheck(this, Cache)
@@ -559,12 +503,12 @@ class Cache {
559
503
 
560
504
  // 5.4.2
561
505
  for (const request of requests) {
562
- const requestObject = new Request('https://a')
563
- requestObject[kState] = request
564
- requestObject[kHeaders][kHeadersList] = request.headersList
565
- requestObject[kHeaders][kGuard] = 'immutable'
566
- requestObject[kRealm] = request.client
567
-
506
+ const requestObject = fromInnerRequest(
507
+ request,
508
+ new AbortController().signal,
509
+ 'immutable',
510
+ { settingsObject: request.client }
511
+ )
568
512
  // 5.4.2.1
569
513
  requestList.push(requestObject)
570
514
  }
@@ -789,6 +733,72 @@ class Cache {
789
733
 
790
734
  return true
791
735
  }
736
+
737
+ #internalMatchAll (request, options, maxResponses = Infinity) {
738
+ // 1.
739
+ let r = null
740
+
741
+ // 2.
742
+ if (request !== undefined) {
743
+ if (request instanceof Request) {
744
+ // 2.1.1
745
+ r = request[kState]
746
+
747
+ // 2.1.2
748
+ if (r.method !== 'GET' && !options.ignoreMethod) {
749
+ return []
750
+ }
751
+ } else if (typeof request === 'string') {
752
+ // 2.2.1
753
+ r = new Request(request)[kState]
754
+ }
755
+ }
756
+
757
+ // 5.
758
+ // 5.1
759
+ const responses = []
760
+
761
+ // 5.2
762
+ if (request === undefined) {
763
+ // 5.2.1
764
+ for (const requestResponse of this.#relevantRequestResponseList) {
765
+ responses.push(requestResponse[1])
766
+ }
767
+ } else { // 5.3
768
+ // 5.3.1
769
+ const requestResponses = this.#queryCache(r, options)
770
+
771
+ // 5.3.2
772
+ for (const requestResponse of requestResponses) {
773
+ responses.push(requestResponse[1])
774
+ }
775
+ }
776
+
777
+ // 5.4
778
+ // We don't implement CORs so we don't need to loop over the responses, yay!
779
+
780
+ // 5.5.1
781
+ const responseList = []
782
+
783
+ // 5.5.2
784
+ for (const response of responses) {
785
+ // 5.5.2.1
786
+ const responseObject = new Response(kConstruct)
787
+ responseObject[kState] = response
788
+ responseObject[kHeaders] = new Headers(kConstruct)
789
+ responseObject[kHeaders][kHeadersList] = response.headersList
790
+ responseObject[kHeaders][kGuard] = 'immutable'
791
+
792
+ responseList.push(responseObject.clone())
793
+
794
+ if (responseList.length >= maxResponses) {
795
+ break
796
+ }
797
+ }
798
+
799
+ // 6.
800
+ return Object.freeze(responseList)
801
+ }
792
802
  }
793
803
 
794
804
  Object.defineProperties(Cache.prototype, {
@@ -114,7 +114,7 @@ class CacheStorage {
114
114
 
115
115
  /**
116
116
  * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
117
- * @returns {string[]}
117
+ * @returns {Promise<string[]>}
118
118
  */
119
119
  async keys () {
120
120
  webidl.brandCheck(this, CacheStorage)
package/lib/client.js CHANGED
@@ -1769,7 +1769,7 @@ function writeH2 (client, session, request) {
1769
1769
 
1770
1770
  session.ref()
1771
1771
 
1772
- const shouldEndStream = method === 'GET' || method === 'HEAD'
1772
+ const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
1773
1773
  if (expectContinue) {
1774
1774
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
1775
1775
  stream = session.request(headers, { endStream: shouldEndStream, signal })
package/lib/core/util.js CHANGED
@@ -349,14 +349,8 @@ function validateHandler (handler, method, upgrade) {
349
349
  // A body is disturbed if it has been read from and it cannot
350
350
  // be re-used without losing state or data.
351
351
  function isDisturbed (body) {
352
- return !!(body && (
353
- stream.isDisturbed
354
- ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
355
- : body[kBodyUsed] ||
356
- body.readableDidRead ||
357
- (body._readableState && body._readableState.dataEmitted) ||
358
- isReadableAborted(body)
359
- ))
352
+ // TODO (fix): Why is body[kBodyUsed] needed?
353
+ return !!(body && (stream.isDisturbed(body) || body[kBodyUsed]))
360
354
  }
361
355
 
362
356
  function isErrored (body) {