undici 7.24.4 → 7.24.6

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
@@ -154,6 +154,57 @@ const { statusCode, body } = await request('https://api.example.com/data');
154
154
  const data = await body.json();
155
155
  ```
156
156
 
157
+ ### Keep `fetch` and `FormData` together
158
+
159
+ When you send a `FormData` body, keep `fetch` and `FormData` from the same
160
+ implementation.
161
+
162
+ Use one of these patterns:
163
+
164
+ ```js
165
+ // Built-in globals
166
+ const body = new FormData()
167
+ body.set('name', 'some')
168
+ await fetch('https://example.com', {
169
+ method: 'POST',
170
+ body
171
+ })
172
+ ```
173
+
174
+ ```js
175
+ // undici module imports
176
+ import { fetch, FormData } from 'undici'
177
+
178
+ const body = new FormData()
179
+ body.set('name', 'some')
180
+ await fetch('https://example.com', {
181
+ method: 'POST',
182
+ body
183
+ })
184
+ ```
185
+
186
+ If you want the installed `undici` package to provide the globals, call
187
+ `install()` first:
188
+
189
+ ```js
190
+ import { install } from 'undici'
191
+
192
+ install()
193
+
194
+ const body = new FormData()
195
+ body.set('name', 'some')
196
+ await fetch('https://example.com', {
197
+ method: 'POST',
198
+ body
199
+ })
200
+ ```
201
+
202
+ `install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and
203
+ `FormData` implementations with undici's versions, so they all match.
204
+
205
+ Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
206
+ with the built-in global `fetch()`.
207
+
157
208
  ### Version Compatibility
158
209
 
159
210
  You can check which version of undici is bundled with your Node.js version:
@@ -263,6 +314,11 @@ The `install()` function adds the following classes to `globalThis`:
263
314
  - `CloseEvent`, `ErrorEvent`, `MessageEvent` - WebSocket events
264
315
  - `EventSource` - Server-sent events client
265
316
 
317
+ When you call `install()`, these globals come from the same undici
318
+ implementation. For example, global `fetch` and global `FormData` will both be
319
+ undici's versions, which is the recommended setup if you want to use undici
320
+ through globals.
321
+
266
322
  This is useful for:
267
323
  - Polyfilling environments that don't have fetch
268
324
  - Ensuring consistent fetch behavior across different Node.js versions
@@ -182,22 +182,24 @@ diagnosticsChannel.channel('undici:websocket:open').subscribe(({
182
182
  console.log(websocket) // the WebSocket instance
183
183
 
184
184
  // Handshake response details
185
- console.log(handshakeResponse.status) // 101 for successful WebSocket upgrade
186
- console.log(handshakeResponse.statusText) // 'Switching Protocols'
185
+ console.log(handshakeResponse.status) // 101 for HTTP/1.1, 200 for HTTP/2 extended CONNECT
186
+ console.log(handshakeResponse.statusText) // 'Switching Protocols' for HTTP/1.1, commonly 'OK' for HTTP/2 in Node.js
187
187
  console.log(handshakeResponse.headers) // Object containing response headers
188
188
  })
189
189
  ```
190
190
 
191
191
  ### Handshake Response Object
192
192
 
193
- The `handshakeResponse` object contains the HTTP response that upgraded the connection to WebSocket:
193
+ The `handshakeResponse` object contains the HTTP response that established the WebSocket connection:
194
194
 
195
- - `status` (number): The HTTP status code (101 for successful WebSocket upgrade)
196
- - `statusText` (string): The HTTP status message ('Switching Protocols' for successful upgrade)
195
+ - `status` (number): The HTTP status code (`101` for HTTP/1.1 upgrade, `200` for HTTP/2 extended CONNECT)
196
+ - `statusText` (string): The HTTP status message (`'Switching Protocols'` for HTTP/1.1, commonly `'OK'` for HTTP/2 in Node.js)
197
197
  - `headers` (object): The HTTP response headers from the server, including:
198
+ - `sec-websocket-accept` and other WebSocket-related headers
198
199
  - `upgrade: 'websocket'`
199
200
  - `connection: 'upgrade'`
200
- - `sec-websocket-accept` and other WebSocket-related headers
201
+
202
+ The `upgrade` and `connection` headers are only present for HTTP/1.1 handshakes.
201
203
 
202
204
  This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection.
203
205
 
@@ -10,6 +10,14 @@ This API is implemented as per the standard, you can find documentation on [MDN]
10
10
 
11
11
  If any parameters are passed to the FormData constructor other than `undefined`, an error will be thrown. Other parameters are ignored.
12
12
 
13
+ When you use `FormData` as a request body, keep `fetch` and `FormData` from the
14
+ same implementation. Use the built-in global `FormData` with the built-in
15
+ global `fetch()`, and use `undici`'s `FormData` with `undici.fetch()`.
16
+
17
+ If you want the installed `undici` package to provide the globals, call
18
+ [`install()`](/docs/api/GlobalInstallation.md) so `fetch`, `Headers`,
19
+ `Response`, `Request`, and `FormData` are installed together as a matching set.
20
+
13
21
  ## Response
14
22
 
15
23
  This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Response)
@@ -43,6 +43,54 @@ The `install()` function adds the following classes to `globalThis`:
43
43
  | `MessageEvent` | WebSocket message event |
44
44
  | `EventSource` | Server-sent events client |
45
45
 
46
+ ## Using `FormData` with `fetch`
47
+
48
+ If you send a `FormData` body, use matching implementations for `fetch` and
49
+ `FormData`.
50
+
51
+ These two patterns are safe:
52
+
53
+ ```js
54
+ // Built-in globals from Node.js
55
+ const body = new FormData()
56
+ await fetch('https://example.com', {
57
+ method: 'POST',
58
+ body
59
+ })
60
+ ```
61
+
62
+ ```js
63
+ // Globals installed from the undici package
64
+ import { install } from 'undici'
65
+
66
+ install()
67
+
68
+ const body = new FormData()
69
+ await fetch('https://example.com', {
70
+ method: 'POST',
71
+ body
72
+ })
73
+ ```
74
+
75
+ After `install()`, `fetch`, `Headers`, `Response`, `Request`, and `FormData`
76
+ all come from the installed `undici` package, so they work as a matching set.
77
+
78
+ If you do not want to install globals, import both from `undici` instead:
79
+
80
+ ```js
81
+ import { fetch, FormData } from 'undici'
82
+
83
+ const body = new FormData()
84
+ await fetch('https://example.com', {
85
+ method: 'POST',
86
+ body
87
+ })
88
+ ```
89
+
90
+ Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
91
+ with the built-in global `fetch()`. Keeping them paired avoids surprising
92
+ multipart behavior across Node.js and undici versions.
93
+
46
94
  ## Use Cases
47
95
 
48
96
  Global installation is useful for:
@@ -19,6 +19,93 @@ When you install undici from npm, you get the full library with all of its
19
19
  additional APIs, and potentially a newer release than what your Node.js version
20
20
  bundles.
21
21
 
22
+ ## Keep `fetch` and `FormData` from the same implementation
23
+
24
+ When you send a `FormData` body, keep `fetch` and `FormData` together from the
25
+ same implementation.
26
+
27
+ Use one of these patterns:
28
+
29
+ ### Built-in globals
30
+
31
+ ```js
32
+ const body = new FormData()
33
+ body.set('name', 'some')
34
+ body.set('someOtherProperty', '8000')
35
+
36
+ await fetch('https://example.com', {
37
+ method: 'POST',
38
+ body
39
+ })
40
+ ```
41
+
42
+ ### `undici` module imports
43
+
44
+ ```js
45
+ import { fetch, FormData } from 'undici'
46
+
47
+ const body = new FormData()
48
+ body.set('name', 'some')
49
+ body.set('someOtherProperty', '8000')
50
+
51
+ await fetch('https://example.com', {
52
+ method: 'POST',
53
+ body
54
+ })
55
+ ```
56
+
57
+ ### `undici.install()` globals
58
+
59
+ If you want the installed `undici` package to provide the globals, call
60
+ [`install()`](/docs/api/GlobalInstallation.md):
61
+
62
+ ```js
63
+ import { install } from 'undici'
64
+
65
+ install()
66
+
67
+ const body = new FormData()
68
+ body.set('name', 'some')
69
+ body.set('someOtherProperty', '8000')
70
+
71
+ await fetch('https://example.com', {
72
+ method: 'POST',
73
+ body
74
+ })
75
+ ```
76
+
77
+ `install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and
78
+ `FormData` implementations with undici's versions, and also installs undici's
79
+ `WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and `EventSource`
80
+ globals.
81
+
82
+ Avoid mixing implementations in the same request, for example:
83
+
84
+ ```js
85
+ import { fetch } from 'undici'
86
+
87
+ const body = new FormData()
88
+
89
+ await fetch('https://example.com', {
90
+ method: 'POST',
91
+ body
92
+ })
93
+ ```
94
+
95
+ ```js
96
+ import { FormData } from 'undici'
97
+
98
+ const body = new FormData()
99
+
100
+ await fetch('https://example.com', {
101
+ method: 'POST',
102
+ body
103
+ })
104
+ ```
105
+
106
+ Those combinations may behave differently across Node.js and undici versions.
107
+ Using matching pairs keeps multipart handling predictable.
108
+
22
109
  ## When you do NOT need to install undici
23
110
 
24
111
  If all of the following are true, you can rely on the built-in globals and skip
@@ -119,12 +206,12 @@ You can always check the exact bundled version at runtime with
119
206
  `process.versions.undici`.
120
207
 
121
208
  Installing undici from npm does not replace the built-in globals. If you want
122
- your installed version to override the global `fetch`, use
123
- [`setGlobalDispatcher`](/docs/api/GlobalInstallation.md) or import `fetch`
209
+ your installed version to replace the global `fetch` and related classes, use
210
+ [`install()`](/docs/api/GlobalInstallation.md). Otherwise, import `fetch`
124
211
  directly from `'undici'`:
125
212
 
126
213
  ```js
127
- import { fetch } from 'undici'; // uses your installed version, not the built-in
214
+ import { fetch } from 'undici' // uses your installed version, not the built-in
128
215
  ```
129
216
 
130
217
  ## Further reading
@@ -18,3 +18,46 @@ const agent = new Agent({
18
18
 
19
19
  setGlobalDispatcher(agent)
20
20
  ```
21
+
22
+ ## Guarding against unexpected disconnects
23
+
24
+ Undici's `Client` automatically reconnects after a socket error. This means
25
+ a test can silently disconnect, reconnect, and still pass. Unfortunately,
26
+ this could mask bugs like unexpected parser errors or protocol violations.
27
+ To catch these silent reconnections, add a disconnect guard after creating
28
+ a `Client`:
29
+
30
+ ```js
31
+ const { Client } = require('undici')
32
+ const { test, after } = require('node:test')
33
+ const { tspl } = require('@matteo.collina/tspl')
34
+
35
+ test('example with disconnect guard', async (t) => {
36
+ t = tspl(t, { plan: 1 })
37
+
38
+ const client = new Client('http://localhost:3000')
39
+ after(() => client.close())
40
+
41
+ client.on('disconnect', () => {
42
+ if (!client.closed && !client.destroyed) {
43
+ t.fail('unexpected disconnect')
44
+ }
45
+ })
46
+
47
+ // ... test logic ...
48
+ })
49
+ ```
50
+
51
+ `client.close()` and `client.destroy()` both emit `'disconnect'` events, but
52
+ those are expected. The guard only fails when a disconnect happens during the
53
+ active test (i.e., `!client.closed && !client.destroyed` is true).
54
+
55
+ Skip the guard for tests where a disconnect is expected behavior, such as:
56
+
57
+ - Signal aborts (`signal.emit('abort')`, `ac.abort()`)
58
+ - Server-side destruction (`res.destroy()`, `req.socket.destroy()`)
59
+ - Client-side body destruction mid-stream (`data.body.destroy()`)
60
+ - Timeout errors (`HeadersTimeoutError`, `BodyTimeoutError`)
61
+ - Successful upgrades (the socket is detached from the `Client`)
62
+ - Retry/reconnect tests where the disconnect triggers the retry
63
+ - HTTP parser errors from malformed responses (`HTTPParserError`)
@@ -177,10 +177,12 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) {
177
177
 
178
178
  diagnosticsChannel.subscribe('undici:websocket:open',
179
179
  evt => {
180
- const {
181
- address: { address, port }
182
- } = evt
183
- debugLog('connection opened %s%s', address, port ? `:${port}` : '')
180
+ if (evt.address != null) {
181
+ const { address, port } = evt.address
182
+ debugLog('connection opened %s%s', address, port ? `:${port}` : '')
183
+ } else {
184
+ debugLog('connection opened')
185
+ }
184
186
  })
185
187
 
186
188
  diagnosticsChannel.subscribe('undici:websocket:close',
@@ -412,13 +412,21 @@ function processHeader (request, key, val) {
412
412
  } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
413
413
  throw new InvalidArgumentError(`invalid ${headerName} header`)
414
414
  } else if (headerName === 'connection') {
415
- const value = typeof val === 'string' ? val.toLowerCase() : null
416
- if (value !== 'close' && value !== 'keep-alive') {
415
+ // Per RFC 7230 Section 6.1, Connection header can contain
416
+ // a comma-separated list of connection option tokens (header names)
417
+ const value = typeof val === 'string' ? val : null
418
+ if (value === null) {
417
419
  throw new InvalidArgumentError('invalid connection header')
418
420
  }
419
421
 
420
- if (value === 'close') {
421
- request.reset = true
422
+ for (const token of value.toLowerCase().split(',')) {
423
+ const trimmed = token.trim()
424
+ if (!isValidHTTPToken(trimmed)) {
425
+ throw new InvalidArgumentError('invalid connection header')
426
+ }
427
+ if (trimmed === 'close') {
428
+ request.reset = true
429
+ }
422
430
  }
423
431
  } else if (headerName === 'expect') {
424
432
  throw new NotSupportedError('expect header not supported')
package/lib/core/util.js CHANGED
@@ -440,19 +440,39 @@ function parseHeaders (headers, obj) {
440
440
  const key = headerNameToString(headers[i])
441
441
  let val = obj[key]
442
442
 
443
- if (val) {
444
- if (typeof val === 'string') {
445
- val = [val]
446
- obj[key] = val
447
- }
448
- val.push(headers[i + 1].toString('latin1'))
449
- } else {
450
- const headersValue = headers[i + 1]
451
- if (typeof headersValue === 'string') {
452
- obj[key] = headersValue
443
+ if (val !== undefined) {
444
+ if (!Object.hasOwn(obj, key)) {
445
+ const headersValue = typeof headers[i + 1] === 'string'
446
+ ? headers[i + 1]
447
+ : Array.isArray(headers[i + 1])
448
+ ? headers[i + 1].map(x => x.toString('latin1'))
449
+ : headers[i + 1].toString('latin1')
450
+
451
+ if (key === '__proto__') {
452
+ Object.defineProperty(obj, key, {
453
+ value: headersValue,
454
+ enumerable: true,
455
+ configurable: true,
456
+ writable: true
457
+ })
458
+ } else {
459
+ obj[key] = headersValue
460
+ }
453
461
  } else {
454
- obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('latin1')) : headersValue.toString('latin1')
462
+ if (typeof val === 'string') {
463
+ val = [val]
464
+ obj[key] = val
465
+ }
466
+ val.push(headers[i + 1].toString('latin1'))
455
467
  }
468
+ } else {
469
+ const headersValue = typeof headers[i + 1] === 'string'
470
+ ? headers[i + 1]
471
+ : Array.isArray(headers[i + 1])
472
+ ? headers[i + 1].map(x => x.toString('latin1'))
473
+ : headers[i + 1].toString('latin1')
474
+
475
+ obj[key] = headersValue
456
476
  }
457
477
  }
458
478
 
@@ -452,65 +452,70 @@ function connect (client) {
452
452
  })
453
453
  }
454
454
 
455
- client[kConnector]({
456
- host,
457
- hostname,
458
- protocol,
459
- port,
460
- servername: client[kServerName],
461
- localAddress: client[kLocalAddress]
462
- }, (err, socket) => {
463
- if (err) {
464
- handleConnectError(client, err, { host, hostname, protocol, port })
465
- client[kResume]()
466
- return
467
- }
455
+ try {
456
+ client[kConnector]({
457
+ host,
458
+ hostname,
459
+ protocol,
460
+ port,
461
+ servername: client[kServerName],
462
+ localAddress: client[kLocalAddress]
463
+ }, (err, socket) => {
464
+ if (err) {
465
+ handleConnectError(client, err, { host, hostname, protocol, port })
466
+ client[kResume]()
467
+ return
468
+ }
468
469
 
469
- if (client.destroyed) {
470
- util.destroy(socket.on('error', noop), new ClientDestroyedError())
471
- client[kResume]()
472
- return
473
- }
470
+ if (client.destroyed) {
471
+ util.destroy(socket.on('error', noop), new ClientDestroyedError())
472
+ client[kResume]()
473
+ return
474
+ }
474
475
 
475
- assert(socket)
476
+ assert(socket)
476
477
 
477
- try {
478
- client[kHTTPContext] = socket.alpnProtocol === 'h2'
479
- ? connectH2(client, socket)
480
- : connectH1(client, socket)
481
- } catch (err) {
482
- socket.destroy().on('error', noop)
483
- handleConnectError(client, err, { host, hostname, protocol, port })
484
- client[kResume]()
485
- return
486
- }
478
+ try {
479
+ client[kHTTPContext] = socket.alpnProtocol === 'h2'
480
+ ? connectH2(client, socket)
481
+ : connectH1(client, socket)
482
+ } catch (err) {
483
+ socket.destroy().on('error', noop)
484
+ handleConnectError(client, err, { host, hostname, protocol, port })
485
+ client[kResume]()
486
+ return
487
+ }
487
488
 
488
- client[kConnecting] = false
489
-
490
- socket[kCounter] = 0
491
- socket[kMaxRequests] = client[kMaxRequests]
492
- socket[kClient] = client
493
- socket[kError] = null
494
-
495
- if (channels.connected.hasSubscribers) {
496
- channels.connected.publish({
497
- connectParams: {
498
- host,
499
- hostname,
500
- protocol,
501
- port,
502
- version: client[kHTTPContext]?.version,
503
- servername: client[kServerName],
504
- localAddress: client[kLocalAddress]
505
- },
506
- connector: client[kConnector],
507
- socket
508
- })
509
- }
489
+ client[kConnecting] = false
490
+
491
+ socket[kCounter] = 0
492
+ socket[kMaxRequests] = client[kMaxRequests]
493
+ socket[kClient] = client
494
+ socket[kError] = null
495
+
496
+ if (channels.connected.hasSubscribers) {
497
+ channels.connected.publish({
498
+ connectParams: {
499
+ host,
500
+ hostname,
501
+ protocol,
502
+ port,
503
+ version: client[kHTTPContext]?.version,
504
+ servername: client[kServerName],
505
+ localAddress: client[kLocalAddress]
506
+ },
507
+ connector: client[kConnector],
508
+ socket
509
+ })
510
+ }
510
511
 
511
- client.emit('connect', client[kUrl], [client])
512
+ client.emit('connect', client[kUrl], [client])
513
+ client[kResume]()
514
+ })
515
+ } catch (err) {
516
+ handleConnectError(client, err, { host, hostname, protocol, port })
512
517
  client[kResume]()
513
- })
518
+ }
514
519
  }
515
520
 
516
521
  function handleConnectError (client, err, { host, hostname, protocol, port }) {
@@ -135,7 +135,7 @@ class CacheHandler {
135
135
  }
136
136
 
137
137
  const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
138
- if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
138
+ if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
139
139
  return downstreamOnHeaders()
140
140
  }
141
141
 
@@ -340,8 +340,9 @@ class CacheHandler {
340
340
  * @param {number} statusCode
341
341
  * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
342
342
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
343
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
343
344
  */
344
- function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
345
+ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
345
346
  // Status code must be final and understood.
346
347
  if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
347
348
  return false
@@ -372,8 +373,16 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
372
373
  }
373
374
 
374
375
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
375
- if (resHeaders.authorization) {
376
- if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') {
376
+ if (reqHeaders?.authorization) {
377
+ if (
378
+ !cacheControlDirectives.public &&
379
+ !cacheControlDirectives['s-maxage'] &&
380
+ !cacheControlDirectives['must-revalidate']
381
+ ) {
382
+ return false
383
+ }
384
+
385
+ if (typeof reqHeaders.authorization !== 'string') {
377
386
  return false
378
387
  }
379
388
 
@@ -493,10 +502,18 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
493
502
  staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
494
503
  }
495
504
 
496
- if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
505
+ if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
497
506
  immutable = now + 31536000000
498
507
  }
499
508
 
509
+ // When no stale directives or immutable flag, add a revalidation buffer
510
+ // equal to the freshness lifetime so the entry survives past staleAt long
511
+ // enough to be revalidated instead of silently disappearing.
512
+ if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
513
+ const freshnessLifetime = staleAt - now
514
+ return staleAt + freshnessLifetime
515
+ }
516
+
500
517
  return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
501
518
  }
502
519
 
@@ -27,5 +27,6 @@ module.exports = {
27
27
  kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'),
28
28
  kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'),
29
29
  kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'),
30
- kMockCallHistoryAddLog: Symbol('mock call history add log')
30
+ kMockCallHistoryAddLog: Symbol('mock call history add log'),
31
+ kTotalDispatchCount: Symbol('total dispatch count')
31
32
  }
@@ -6,7 +6,8 @@ const {
6
6
  kMockAgent,
7
7
  kOriginalDispatch,
8
8
  kOrigin,
9
- kGetNetConnect
9
+ kGetNetConnect,
10
+ kTotalDispatchCount
10
11
  } = require('./mock-symbols')
11
12
  const { serializePathWithQuery } = require('../core/util')
12
13
  const { STATUS_CODES } = require('node:http')
@@ -206,6 +207,8 @@ function addMockDispatch (mockDispatches, key, data, opts) {
206
207
  const replyData = typeof data === 'function' ? { callback: data } : { ...data }
207
208
  const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
208
209
  mockDispatches.push(newMockDispatch)
210
+ // Track total number of intercepts ever registered for better error messages
211
+ mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1
209
212
  return newMockDispatch
210
213
  }
211
214
 
@@ -401,13 +404,16 @@ function buildMockDispatch () {
401
404
  } catch (error) {
402
405
  if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
403
406
  const netConnect = agent[kGetNetConnect]()
407
+ const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length
408
+ const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length
409
+ const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined`
404
410
  if (netConnect === false) {
405
- throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
411
+ throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`)
406
412
  }
407
413
  if (checkNetConnect(netConnect, origin)) {
408
414
  originalDispatch.call(this, opts, handler)
409
415
  } else {
410
- throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`)
416
+ throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`)
411
417
  }
412
418
  } else {
413
419
  throw error
@@ -6,10 +6,10 @@ const { makeEntry } = require('./formdata')
6
6
  const { webidl } = require('../webidl')
7
7
  const assert = require('node:assert')
8
8
  const { isomorphicDecode } = require('../infra')
9
- const { utf8DecodeBytes } = require('../../encoding')
10
9
 
11
10
  const dd = Buffer.from('--')
12
11
  const decoder = new TextDecoder()
12
+ const decoderIgnoreBOM = new TextDecoder('utf-8', { ignoreBOM: true })
13
13
 
14
14
  /**
15
15
  * @param {string} chars
@@ -188,7 +188,7 @@ function multipartFormDataParser (input, mimeType) {
188
188
  // 5.11. Otherwise:
189
189
 
190
190
  // 5.11.1. Let value be the UTF-8 decoding without BOM of body.
191
- value = utf8DecodeBytes(Buffer.from(body))
191
+ value = decoderIgnoreBOM.decode(Buffer.from(body))
192
192
  }
193
193
 
194
194
  // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
@@ -25,6 +25,18 @@ const { SendQueue } = require('./sender')
25
25
  const { WebsocketFrameSend } = require('./frame')
26
26
  const { channels } = require('../../core/diagnostics')
27
27
 
28
+ function getSocketAddress (socket) {
29
+ if (typeof socket?.address === 'function') {
30
+ return socket.address()
31
+ }
32
+
33
+ if (typeof socket?.session?.socket?.address === 'function') {
34
+ return socket.session.socket.address()
35
+ }
36
+
37
+ return null
38
+ }
39
+
28
40
  /**
29
41
  * @typedef {object} Handler
30
42
  * @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
@@ -491,7 +503,7 @@ class WebSocket extends EventTarget {
491
503
  // Convert headers to a plain object for the event
492
504
  const headers = response.headersList.entries
493
505
  channels.open.publish({
494
- address: response.socket.address(),
506
+ address: getSocketAddress(response.socket),
495
507
  protocol: this.#protocol,
496
508
  extensions: this.#extensions,
497
509
  websocket: this,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.24.4",
3
+ "version": "7.24.6",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -129,7 +129,7 @@
129
129
  "node-forge": "^1.3.1",
130
130
  "proxy": "^2.1.1",
131
131
  "tsd": "^0.33.0",
132
- "typescript": "^5.6.2",
132
+ "typescript": "^6.0.2",
133
133
  "ws": "^8.11.0"
134
134
  },
135
135
  "engines": {