undici 7.2.3 → 7.4.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
@@ -337,7 +337,8 @@ See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-
337
337
 
338
338
  * dispatcher `Dispatcher`
339
339
 
340
- Sets the global dispatcher used by Common API Methods.
340
+ Sets the global dispatcher used by Common API Methods. Global dispatcher is shared among compatible undici modules,
341
+ including undici that is bundled internally with node.js.
341
342
 
342
343
  ### `undici.getGlobalDispatcher()`
343
344
 
@@ -210,7 +210,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
210
210
  * **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string []>, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
211
211
  * **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
212
212
  * **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
213
- * **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
213
+ * **onResponseError** `(controller: DispatchController, error: Error) => void` - Invoked when an error has occurred. May not throw.
214
214
 
215
215
  #### Example 1 - Dispatch GET request
216
216
 
@@ -1,7 +1,5 @@
1
1
  # Class: EnvHttpProxyAgent
2
2
 
3
- Stability: Experimental.
4
-
5
3
  Extends: `undici.Dispatcher`
6
4
 
7
5
  EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `http_proxy`, `https_proxy`, and `no_proxy` and sets up the proxy agents accordingly. When `http_proxy` and `https_proxy` are set, `http_proxy` is used for HTTP requests and `https_proxy` is used for HTTPS requests. If only `http_proxy` is set, `http_proxy` is used for both HTTP and HTTPS requests. If only `https_proxy` is set, it is only used for HTTPS requests.
@@ -28,6 +28,7 @@ import { errors } from 'undici'
28
28
  | `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
29
29
  | `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
30
30
 
31
+ Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === '<error_code>'` instead to avoid inconsistencies.
31
32
  ### `SocketError`
32
33
 
33
34
  The `SocketError` has a `.socket` property which holds socket metadata:
package/index-fetch.js CHANGED
@@ -26,6 +26,9 @@ module.exports.createFastMessageEvent = createFastMessageEvent
26
26
 
27
27
  module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource
28
28
 
29
+ const api = require('./lib/api')
30
+ const Dispatcher = require('./lib/dispatcher/dispatcher')
31
+ Object.assign(Dispatcher.prototype, api)
29
32
  // Expose the fetch implementation to be enabled in Node.js core via a flag
30
33
  module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent
31
34
  module.exports.getGlobalDispatcher = getGlobalDispatcher
@@ -79,7 +79,13 @@ class MemoryCacheStore {
79
79
  const entry = this.#entries.get(topLevelKey)?.find((entry) => (
80
80
  entry.deleteAt > now &&
81
81
  entry.method === key.method &&
82
- (entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers?.[headerName]))
82
+ (entry.vary == null || Object.keys(entry.vary).every(headerName => {
83
+ if (entry.vary[headerName] === null) {
84
+ return key.headers[headerName] === undefined
85
+ }
86
+
87
+ return entry.vary[headerName] === key.headers[headerName]
88
+ }))
83
89
  ))
84
90
 
85
91
  return entry == null
@@ -15,11 +15,18 @@ const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
15
15
  * @implements {CacheStore}
16
16
  *
17
17
  * @typedef {{
18
- * id: Readonly<number>
19
- * headers?: Record<string, string | string[]>
20
- * vary?: string | object
21
- * body: string
22
- * } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
18
+ * id: Readonly<number>,
19
+ * body?: Uint8Array
20
+ * statusCode: number
21
+ * statusMessage: string
22
+ * headers?: string
23
+ * vary?: string
24
+ * etag?: string
25
+ * cacheControlDirectives?: string
26
+ * cachedAt: number
27
+ * staleAt: number
28
+ * deleteAt: number
29
+ * }} SqliteStoreValue
23
30
  */
24
31
  module.exports = class SqliteCacheStore {
25
32
  #maxEntrySize = MAX_ENTRY_SIZE
@@ -61,7 +68,7 @@ module.exports = class SqliteCacheStore {
61
68
  #countEntriesQuery
62
69
 
63
70
  /**
64
- * @type {import('node:sqlite').StatementSync}
71
+ * @type {import('node:sqlite').StatementSync | null}
65
72
  */
66
73
  #deleteOldValuesQuery
67
74
 
@@ -163,8 +170,7 @@ module.exports = class SqliteCacheStore {
163
170
  etag = ?,
164
171
  cacheControlDirectives = ?,
165
172
  cachedAt = ?,
166
- staleAt = ?,
167
- deleteAt = ?
173
+ staleAt = ?
168
174
  WHERE
169
175
  id = ?
170
176
  `)
@@ -182,9 +188,8 @@ module.exports = class SqliteCacheStore {
182
188
  cacheControlDirectives,
183
189
  vary,
184
190
  cachedAt,
185
- staleAt,
186
- deleteAt
187
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
191
+ staleAt
192
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
188
193
  `)
189
194
 
190
195
  this.#deleteByUrlQuery = this.#db.prepare(
@@ -219,36 +224,78 @@ module.exports = class SqliteCacheStore {
219
224
 
220
225
  /**
221
226
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
222
- * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
227
+ * @returns {(import('../../types/cache-interceptor.d.ts').default.GetResult & { body?: Buffer }) | undefined}
223
228
  */
224
229
  get (key) {
225
230
  assertCacheKey(key)
226
231
 
227
232
  const value = this.#findValue(key)
233
+ return value
234
+ ? {
235
+ body: value.body ? Buffer.from(value.body.buffer, value.body.byteOffset, value.body.byteLength) : undefined,
236
+ statusCode: value.statusCode,
237
+ statusMessage: value.statusMessage,
238
+ headers: value.headers ? JSON.parse(value.headers) : undefined,
239
+ etag: value.etag ? value.etag : undefined,
240
+ vary: value.vary ? JSON.parse(value.vary) : undefined,
241
+ cacheControlDirectives: value.cacheControlDirectives
242
+ ? JSON.parse(value.cacheControlDirectives)
243
+ : undefined,
244
+ cachedAt: value.cachedAt,
245
+ staleAt: value.staleAt,
246
+ deleteAt: value.deleteAt
247
+ }
248
+ : undefined
249
+ }
228
250
 
229
- if (!value) {
230
- return undefined
231
- }
251
+ /**
252
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
253
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>}} value
254
+ */
255
+ set (key, value) {
256
+ assertCacheKey(key)
232
257
 
233
- /**
234
- * @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
235
- */
236
- const result = {
237
- body: Buffer.from(value.body),
238
- statusCode: value.statusCode,
239
- statusMessage: value.statusMessage,
240
- headers: value.headers ? JSON.parse(value.headers) : undefined,
241
- etag: value.etag ? value.etag : undefined,
242
- vary: value.vary ?? undefined,
243
- cacheControlDirectives: value.cacheControlDirectives
244
- ? JSON.parse(value.cacheControlDirectives)
245
- : undefined,
246
- cachedAt: value.cachedAt,
247
- staleAt: value.staleAt,
248
- deleteAt: value.deleteAt
258
+ const url = this.#makeValueUrl(key)
259
+ const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body
260
+ const size = body?.byteLength
261
+
262
+ if (size && size > this.#maxEntrySize) {
263
+ return
249
264
  }
250
265
 
251
- return result
266
+ const existingValue = this.#findValue(key, true)
267
+ if (existingValue) {
268
+ // Updating an existing response, let's overwrite it
269
+ this.#updateValueQuery.run(
270
+ body,
271
+ value.deleteAt,
272
+ value.statusCode,
273
+ value.statusMessage,
274
+ value.headers ? JSON.stringify(value.headers) : null,
275
+ value.etag ? value.etag : null,
276
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
277
+ value.cachedAt,
278
+ value.staleAt,
279
+ existingValue.id
280
+ )
281
+ } else {
282
+ this.#prune()
283
+ // New response, let's insert it
284
+ this.#insertValueQuery.run(
285
+ url,
286
+ key.method,
287
+ body,
288
+ value.deleteAt,
289
+ value.statusCode,
290
+ value.statusMessage,
291
+ value.headers ? JSON.stringify(value.headers) : null,
292
+ value.etag ? value.etag : null,
293
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
294
+ value.vary ? JSON.stringify(value.vary) : null,
295
+ value.cachedAt,
296
+ value.staleAt
297
+ )
298
+ }
252
299
  }
253
300
 
254
301
  /**
@@ -260,7 +307,6 @@ module.exports = class SqliteCacheStore {
260
307
  assertCacheKey(key)
261
308
  assertCacheValue(value)
262
309
 
263
- const url = this.#makeValueUrl(key)
264
310
  let size = 0
265
311
  /**
266
312
  * @type {Buffer[] | null}
@@ -269,11 +315,8 @@ module.exports = class SqliteCacheStore {
269
315
  const store = this
270
316
 
271
317
  return new Writable({
318
+ decodeStrings: true,
272
319
  write (chunk, encoding, callback) {
273
- if (typeof chunk === 'string') {
274
- chunk = Buffer.from(chunk, encoding)
275
- }
276
-
277
320
  size += chunk.byteLength
278
321
 
279
322
  if (size < store.#maxEntrySize) {
@@ -285,42 +328,7 @@ module.exports = class SqliteCacheStore {
285
328
  callback()
286
329
  },
287
330
  final (callback) {
288
- const existingValue = store.#findValue(key, true)
289
- if (existingValue) {
290
- // Updating an existing response, let's overwrite it
291
- store.#updateValueQuery.run(
292
- Buffer.concat(body),
293
- value.deleteAt,
294
- value.statusCode,
295
- value.statusMessage,
296
- value.headers ? JSON.stringify(value.headers) : null,
297
- value.etag ? value.etag : null,
298
- value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
299
- value.cachedAt,
300
- value.staleAt,
301
- value.deleteAt,
302
- existingValue.id
303
- )
304
- } else {
305
- store.#prune()
306
- // New response, let's insert it
307
- store.#insertValueQuery.run(
308
- url,
309
- key.method,
310
- Buffer.concat(body),
311
- value.deleteAt,
312
- value.statusCode,
313
- value.statusMessage,
314
- value.headers ? JSON.stringify(value.headers) : null,
315
- value.etag ? value.etag : null,
316
- value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
317
- value.vary ? JSON.stringify(value.vary) : null,
318
- value.cachedAt,
319
- value.staleAt,
320
- value.deleteAt
321
- )
322
- }
323
-
331
+ store.set(key, { ...value, body })
324
332
  callback()
325
333
  }
326
334
  })
@@ -344,14 +352,14 @@ module.exports = class SqliteCacheStore {
344
352
 
345
353
  {
346
354
  const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
347
- if (removed > 0) {
355
+ if (removed) {
348
356
  return removed
349
357
  }
350
358
  }
351
359
 
352
360
  {
353
- const removed = this.#deleteOldValuesQuery.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
354
- if (removed > 0) {
361
+ const removed = this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
362
+ if (removed) {
355
363
  return removed
356
364
  }
357
365
  }
@@ -379,7 +387,7 @@ module.exports = class SqliteCacheStore {
379
387
  /**
380
388
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
381
389
  * @param {boolean} [canBeExpired=false]
382
- * @returns {(SqliteStoreValue & { vary?: Record<string, string[]> }) | undefined}
390
+ * @returns {SqliteStoreValue | undefined}
383
391
  */
384
392
  #findValue (key, canBeExpired = false) {
385
393
  const url = this.#makeValueUrl(key)
@@ -403,14 +411,10 @@ module.exports = class SqliteCacheStore {
403
411
  let matches = true
404
412
 
405
413
  if (value.vary) {
406
- if (!headers) {
407
- return undefined
408
- }
414
+ const vary = JSON.parse(value.vary)
409
415
 
410
- value.vary = JSON.parse(value.vary)
411
-
412
- for (const header in value.vary) {
413
- if (!headerValueEquals(headers[header], value.vary[header])) {
416
+ for (const header in vary) {
417
+ if (!headerValueEquals(headers[header], vary[header])) {
414
418
  matches = false
415
419
  break
416
420
  }
@@ -432,18 +436,21 @@ module.exports = class SqliteCacheStore {
432
436
  * @returns {boolean}
433
437
  */
434
438
  function headerValueEquals (lhs, rhs) {
439
+ if (lhs == null && rhs == null) {
440
+ return true
441
+ }
442
+
443
+ if ((lhs == null && rhs != null) ||
444
+ (lhs != null && rhs == null)) {
445
+ return false
446
+ }
447
+
435
448
  if (Array.isArray(lhs) && Array.isArray(rhs)) {
436
449
  if (lhs.length !== rhs.length) {
437
450
  return false
438
451
  }
439
452
 
440
- for (let i = 0; i < lhs.length; i++) {
441
- if (rhs.includes(lhs[i])) {
442
- return false
443
- }
444
- }
445
-
446
- return true
453
+ return lhs.every((x, i) => x === rhs[i])
447
454
  }
448
455
 
449
456
  return lhs === rhs
@@ -10,8 +10,6 @@ const DEFAULT_PORTS = {
10
10
  'https:': 443
11
11
  }
12
12
 
13
- let experimentalWarned = false
14
-
15
13
  class EnvHttpProxyAgent extends DispatcherBase {
16
14
  #noProxyValue = null
17
15
  #noProxyEntries = null
@@ -21,13 +19,6 @@ class EnvHttpProxyAgent extends DispatcherBase {
21
19
  super()
22
20
  this.#opts = opts
23
21
 
24
- if (!experimentalWarned) {
25
- experimentalWarned = true
26
- process.emitWarning('EnvHttpProxyAgent is experimental, expect them to change at any time.', {
27
- code: 'UNDICI-EHPA'
28
- })
29
- }
30
-
31
22
  const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
32
23
 
33
24
  this[kNoProxyAgent] = new Agent(agentOpts)
@@ -124,8 +124,10 @@ function getResponseData (data) {
124
124
  return data
125
125
  } else if (typeof data === 'object') {
126
126
  return JSON.stringify(data)
127
- } else {
127
+ } else if (data) {
128
128
  return data.toString()
129
+ } else {
130
+ return ''
129
131
  }
130
132
  }
131
133
 
package/lib/util/cache.js CHANGED
@@ -26,10 +26,14 @@ function makeCacheKey (opts) {
26
26
  if (typeof key !== 'string' || typeof val !== 'string') {
27
27
  throw new Error('opts.headers is not a valid header map')
28
28
  }
29
- headers[key] = val
29
+ headers[key.toLowerCase()] = val
30
30
  }
31
31
  } else if (typeof opts.headers === 'object') {
32
- headers = opts.headers
32
+ headers = {}
33
+
34
+ for (const key of Object.keys(opts.headers)) {
35
+ headers[key.toLowerCase()] = opts.headers[key]
36
+ }
33
37
  } else {
34
38
  throw new Error('opts.headers is not an object')
35
39
  }
@@ -260,19 +264,16 @@ function parseVaryHeader (varyHeader, headers) {
260
264
  return headers
261
265
  }
262
266
 
263
- const output = /** @type {Record<string, string | string[]>} */ ({})
267
+ const output = /** @type {Record<string, string | string[] | null>} */ ({})
264
268
 
265
269
  const varyingHeaders = typeof varyHeader === 'string'
266
270
  ? varyHeader.split(',')
267
271
  : varyHeader
272
+
268
273
  for (const header of varyingHeaders) {
269
274
  const trimmedHeader = header.trim().toLowerCase()
270
275
 
271
- if (headers[trimmedHeader]) {
272
- output[trimmedHeader] = headers[trimmedHeader]
273
- } else {
274
- return undefined
275
- }
276
+ output[trimmedHeader] = headers[trimmedHeader] ?? null
276
277
  }
277
278
 
278
279
  return output
@@ -23,7 +23,7 @@ try {
23
23
  const crypto = require('node:crypto')
24
24
  random = (max) => crypto.randomInt(0, max)
25
25
  } catch {
26
- random = (max) => Math.floor(Math.random(max))
26
+ random = (max) => Math.floor(Math.random() * max)
27
27
  }
28
28
 
29
29
  const textEncoder = new TextEncoder()
@@ -37,6 +37,14 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
37
37
 
38
38
  const dependentControllerMap = new WeakMap()
39
39
 
40
+ let abortSignalHasEventHandlerLeakWarning
41
+
42
+ try {
43
+ abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0
44
+ } catch {
45
+ abortSignalHasEventHandlerLeakWarning = false
46
+ }
47
+
40
48
  function buildAbort (acRef) {
41
49
  return abort
42
50
 
@@ -424,15 +432,10 @@ class Request {
424
432
  const acRef = new WeakRef(ac)
425
433
  const abort = buildAbort(acRef)
426
434
 
427
- // Third-party AbortControllers may not work with these.
428
- // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
429
- try {
430
- // If the max amount of listeners is equal to the default, increase it
431
- // This is only available in node >= v19.9.0
432
- if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) {
433
- setMaxListeners(1500, signal)
434
- }
435
- } catch {}
435
+ // If the max amount of listeners is equal to the default, increase it
436
+ if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
437
+ setMaxListeners(1500, signal)
438
+ }
436
439
 
437
440
  util.addAbortListener(signal, abort)
438
441
  // The third argument must be a registry key to be unregistered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.2.3",
3
+ "version": "7.4.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": {
@@ -70,7 +70,7 @@ declare namespace CacheHandler {
70
70
  statusCode: number
71
71
  statusMessage: string
72
72
  headers: Record<string, string | string[]>
73
- vary?: Record<string, string | string[]>
73
+ vary?: Record<string, string | string[] | null>
74
74
  etag?: string
75
75
  cacheControlDirectives?: CacheControlDirectives
76
76
  cachedAt: number
@@ -88,9 +88,9 @@ declare namespace CacheHandler {
88
88
  statusCode: number
89
89
  statusMessage: string
90
90
  headers: Record<string, string | string[]>
91
- vary?: Record<string, string | string[]>
91
+ vary?: Record<string, string | string[] | null>
92
92
  etag?: string
93
- body: null | Readable | Iterable<Buffer> | AsyncIterable<Buffer> | Buffer | Iterable<string> | AsyncIterable<string> | string
93
+ body?: Readable | Iterable<Buffer> | AsyncIterable<Buffer> | Buffer | Iterable<string> | AsyncIterable<string> | string
94
94
  cacheControlDirectives: CacheControlDirectives,
95
95
  cachedAt: number
96
96
  staleAt: number