undici 7.13.0 → 7.14.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.
@@ -0,0 +1,158 @@
1
+ 'use strict'
2
+
3
+ const { InvalidArgumentError } = require('../core/errors')
4
+
5
+ /**
6
+ * @typedef {Object} HeaderFilters
7
+ * @property {Set<string>} ignore - Set of headers to ignore for matching
8
+ * @property {Set<string>} exclude - Set of headers to exclude from matching
9
+ * @property {Set<string>} match - Set of headers to match (empty means match
10
+ */
11
+
12
+ /**
13
+ * Creates cached header sets for performance
14
+ *
15
+ * @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers
16
+ * @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers
17
+ */
18
+ function createHeaderFilters (matchOptions = {}) {
19
+ const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions
20
+
21
+ return {
22
+ ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())),
23
+ exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())),
24
+ match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase()))
25
+ }
26
+ }
27
+
28
+ let crypto
29
+ try {
30
+ crypto = require('node:crypto')
31
+ } catch { /* Fallback if crypto is not available */ }
32
+
33
+ /**
34
+ * @callback HashIdFunction
35
+ * @param {string} value - The value to hash
36
+ * @returns {string} - The base64url encoded hash of the value
37
+ */
38
+
39
+ /**
40
+ * Generates a hash for a given value
41
+ * @type {HashIdFunction}
42
+ */
43
+ const hashId = crypto?.hash
44
+ ? (value) => crypto.hash('sha256', value, 'base64url')
45
+ : (value) => Buffer.from(value).toString('base64url')
46
+
47
+ /**
48
+ * @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns
49
+ */
50
+
51
+ /** @typedef {{[key: Lowercase<string>]: string}} NormalizedHeaders */
52
+ /** @typedef {Array<string>} UndiciHeaders */
53
+ /** @typedef {Record<string, string|string[]>} Headers */
54
+
55
+ /**
56
+ * @param {*} headers
57
+ * @returns {headers is UndiciHeaders}
58
+ */
59
+ function isUndiciHeaders (headers) {
60
+ return Array.isArray(headers) && (headers.length & 1) === 0
61
+ }
62
+
63
+ /**
64
+ * Factory function to create a URL exclusion checker
65
+ * @param {Array<string| RegExp>} [excludePatterns=[]] - Array of patterns to exclude
66
+ * @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns
67
+ */
68
+ function isUrlExcludedFactory (excludePatterns = []) {
69
+ if (excludePatterns.length === 0) {
70
+ return () => false
71
+ }
72
+
73
+ return function isUrlExcluded (url) {
74
+ let urlLowerCased
75
+
76
+ for (const pattern of excludePatterns) {
77
+ if (typeof pattern === 'string') {
78
+ if (!urlLowerCased) {
79
+ // Convert URL to lowercase only once
80
+ urlLowerCased = url.toLowerCase()
81
+ }
82
+ // Simple string match (case-insensitive)
83
+ if (urlLowerCased.includes(pattern.toLowerCase())) {
84
+ return true
85
+ }
86
+ } else if (pattern instanceof RegExp) {
87
+ // Regex pattern match
88
+ if (pattern.test(url)) {
89
+ return true
90
+ }
91
+ }
92
+ }
93
+
94
+ return false
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Normalizes headers for consistent comparison
100
+ *
101
+ * @param {Object|UndiciHeaders} headers - Headers to normalize
102
+ * @returns {NormalizedHeaders} - Normalized headers as a lowercase object
103
+ */
104
+ function normalizeHeaders (headers) {
105
+ /** @type {NormalizedHeaders} */
106
+ const normalizedHeaders = {}
107
+
108
+ if (!headers) return normalizedHeaders
109
+
110
+ // Handle array format (undici internal format: [name, value, name, value, ...])
111
+ if (isUndiciHeaders(headers)) {
112
+ for (let i = 0; i < headers.length; i += 2) {
113
+ const key = headers[i]
114
+ const value = headers[i + 1]
115
+ if (key && value !== undefined) {
116
+ // Convert Buffers to strings if needed
117
+ const keyStr = Buffer.isBuffer(key) ? key.toString() : key
118
+ const valueStr = Buffer.isBuffer(value) ? value.toString() : value
119
+ normalizedHeaders[keyStr.toLowerCase()] = valueStr
120
+ }
121
+ }
122
+ return normalizedHeaders
123
+ }
124
+
125
+ // Handle object format
126
+ if (headers && typeof headers === 'object') {
127
+ for (const [key, value] of Object.entries(headers)) {
128
+ if (key && typeof key === 'string') {
129
+ normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
130
+ }
131
+ }
132
+ }
133
+
134
+ return normalizedHeaders
135
+ }
136
+
137
+ const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update'])
138
+
139
+ /** @typedef {typeof validSnapshotModes[number]} SnapshotMode */
140
+
141
+ /**
142
+ * @param {*} mode - The snapshot mode to validate
143
+ * @returns {asserts mode is SnapshotMode}
144
+ */
145
+ function validateSnapshotMode (mode) {
146
+ if (!validSnapshotModes.includes(mode)) {
147
+ throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`)
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ createHeaderFilters,
153
+ hashId,
154
+ isUndiciHeaders,
155
+ normalizeHeaders,
156
+ isUrlExcludedFactory,
157
+ validateSnapshotMode
158
+ }
package/lib/util/cache.js CHANGED
@@ -34,7 +34,7 @@ function makeCacheKey (opts) {
34
34
  * @param {Record<string, string[] | string>}
35
35
  * @returns {Record<string, string[] | string>}
36
36
  */
37
- function normaliseHeaders (opts) {
37
+ function normalizeHeaders (opts) {
38
38
  let headers
39
39
  if (opts.headers == null) {
40
40
  headers = {}
@@ -234,7 +234,7 @@ function parseCacheControlHeader (header) {
234
234
  }
235
235
  }
236
236
  } else {
237
- // Something like `no-cache=some-header`
237
+ // Something like `no-cache="some-header"`
238
238
  if (key in output) {
239
239
  output[key] = output[key].concat(value)
240
240
  } else {
@@ -367,7 +367,7 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
367
367
 
368
368
  module.exports = {
369
369
  makeCacheKey,
370
- normaliseHeaders,
370
+ normalizeHeaders,
371
371
  assertCacheKey,
372
372
  assertCacheValue,
373
373
  parseCacheControlHeader,
@@ -18,7 +18,7 @@ const { createDeferredPromise } = require('../../util/promise')
18
18
  * @property {'delete' | 'put'} type
19
19
  * @property {any} request
20
20
  * @property {any} response
21
- * @property {import('../../types/cache').CacheQueryOptions} options
21
+ * @property {import('../../../types/cache').CacheQueryOptions} options
22
22
  */
23
23
 
24
24
  /**
@@ -452,7 +452,7 @@ class Cache {
452
452
  /**
453
453
  * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
454
454
  * @param {any} request
455
- * @param {import('../../types/cache').CacheQueryOptions} options
455
+ * @param {import('../../../types/cache').CacheQueryOptions} options
456
456
  * @returns {Promise<readonly Request[]>}
457
457
  */
458
458
  async keys (request = undefined, options = {}) {
@@ -670,7 +670,7 @@ class Cache {
670
670
  /**
671
671
  * @see https://w3c.github.io/ServiceWorker/#query-cache
672
672
  * @param {any} requestQuery
673
- * @param {import('../../types/cache').CacheQueryOptions} options
673
+ * @param {import('../../../types/cache').CacheQueryOptions} options
674
674
  * @param {requestResponseList} targetStorage
675
675
  * @returns {requestResponseList}
676
676
  */
@@ -695,7 +695,7 @@ class Cache {
695
695
  * @param {any} requestQuery
696
696
  * @param {any} request
697
697
  * @param {any | null} response
698
- * @param {import('../../types/cache').CacheQueryOptions | undefined} options
698
+ * @param {import('../../../types/cache').CacheQueryOptions | undefined} options
699
699
  * @returns {boolean}
700
700
  */
701
701
  #requestMatchesCachedItem (requestQuery, request, response = null, options) {
@@ -124,10 +124,10 @@ class EventSource extends EventTarget {
124
124
  url = webidl.converters.USVString(url)
125
125
  eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict')
126
126
 
127
- this.#dispatcher = eventSourceInitDict.dispatcher
127
+ this.#dispatcher = eventSourceInitDict.node.dispatcher || eventSourceInitDict.dispatcher
128
128
  this.#state = {
129
129
  lastEventId: '',
130
- reconnectionTime: defaultReconnectionTime
130
+ reconnectionTime: eventSourceInitDict.node.reconnectionTime
131
131
  }
132
132
 
133
133
  // 2. Let settings be ev's relevant settings object.
@@ -472,6 +472,21 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
472
472
  {
473
473
  key: 'dispatcher', // undici only
474
474
  converter: webidl.converters.any
475
+ },
476
+ {
477
+ key: 'node', // undici only
478
+ converter: webidl.dictionaryConverter([
479
+ {
480
+ key: 'reconnectionTime',
481
+ converter: webidl.converters['unsigned long'],
482
+ defaultValue: () => defaultReconnectionTime
483
+ },
484
+ {
485
+ key: 'dispatcher',
486
+ converter: webidl.converters.any
487
+ }
488
+ ]),
489
+ defaultValue: () => ({})
475
490
  }
476
491
  ])
477
492
 
@@ -9,7 +9,7 @@ const nodeUtil = require('node:util')
9
9
  class FormData {
10
10
  #state = []
11
11
 
12
- constructor (form) {
12
+ constructor (form = undefined) {
13
13
  webidl.util.markAsUncloneable(this)
14
14
 
15
15
  if (form !== undefined) {
@@ -22,7 +22,8 @@ const { webidl } = require('../webidl')
22
22
  const { URLSerializer } = require('./data-url')
23
23
  const { kConstruct } = require('../../core/symbols')
24
24
  const assert = require('node:assert')
25
- const { types } = require('node:util')
25
+
26
+ const { isArrayBuffer } = nodeUtil.types
26
27
 
27
28
  const textEncoder = new TextEncoder('utf-8')
28
29
 
@@ -243,6 +244,11 @@ class Response {
243
244
  // 2. Let clonedResponse be the result of cloning this’s response.
244
245
  const clonedResponse = cloneResponse(this.#state)
245
246
 
247
+ // Note: To re-register because of a new stream.
248
+ if (this.#state.body?.stream) {
249
+ streamRegistry.register(this, new WeakRef(this.#state.body.stream))
250
+ }
251
+
246
252
  // 3. Return the result of creating a Response object, given
247
253
  // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
248
254
  return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers))
@@ -353,8 +359,6 @@ function cloneResponse (response) {
353
359
  // result of cloning response’s body.
354
360
  if (response.body != null) {
355
361
  newResponse.body = cloneBody(response.body)
356
-
357
- streamRegistry.register(newResponse, new WeakRef(response.body.stream))
358
362
  }
359
363
 
360
364
  // 4. Return newResponse.
@@ -576,7 +580,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
576
580
  return V
577
581
  }
578
582
 
579
- if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
583
+ if (ArrayBuffer.isView(V) || isArrayBuffer(V)) {
580
584
  return V
581
585
  }
582
586
 
@@ -6,7 +6,7 @@ const { states, opcodes, sentCloseFrameState } = require('../constants')
6
6
  const { webidl } = require('../../webidl')
7
7
  const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
8
8
  const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
9
- const { types } = require('node:util')
9
+ const { isArrayBuffer } = require('node:util/types')
10
10
  const { channels } = require('../../../core/diagnostics')
11
11
  const { WebsocketFrameSend } = require('../frame')
12
12
  const { ByteParser } = require('../receiver')
@@ -210,7 +210,7 @@ class WebSocketStream {
210
210
  let opcode = null
211
211
 
212
212
  // 4. If chunk is a BufferSource ,
213
- if (ArrayBuffer.isView(chunk) || types.isArrayBuffer(chunk)) {
213
+ if (ArrayBuffer.isView(chunk) || isArrayBuffer(chunk)) {
214
214
  // 4.1. Set data to a copy of the bytes given chunk .
215
215
  data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk)
216
216
 
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { isArrayBuffer } = require('node:util/types')
3
4
  const { webidl } = require('../webidl')
4
5
  const { URLSerializer } = require('../fetch/data-url')
5
6
  const { environmentSettingsObject } = require('../fetch/util')
@@ -19,7 +20,6 @@ const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketCon
19
20
  const { ByteParser } = require('./receiver')
20
21
  const { kEnumerableProperty } = require('../../core/util')
21
22
  const { getGlobalDispatcher } = require('../../global')
22
- const { types } = require('node:util')
23
23
  const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
24
24
  const { SendQueue } = require('./sender')
25
25
  const { WebsocketFrameSend } = require('./frame')
@@ -257,7 +257,7 @@ class WebSocket extends EventTarget {
257
257
  this.#sendQueue.add(buffer, () => {
258
258
  this.#bufferedAmount -= buffer.byteLength
259
259
  }, sendHints.text)
260
- } else if (types.isArrayBuffer(data)) {
260
+ } else if (isArrayBuffer(data)) {
261
261
  // If the WebSocket connection is established, and the WebSocket
262
262
  // closing handshake has not yet started, then the user agent must
263
263
  // send a WebSocket Message comprised of data using a binary frame
@@ -482,11 +482,18 @@ class WebSocket extends EventTarget {
482
482
  fireEvent('open', this)
483
483
 
484
484
  if (channels.open.hasSubscribers) {
485
+ // Convert headers to a plain object for the event
486
+ const headers = response.headersList.entries
485
487
  channels.open.publish({
486
488
  address: response.socket.address(),
487
489
  protocol: this.#protocol,
488
490
  extensions: this.#extensions,
489
- websocket: this
491
+ websocket: this,
492
+ handshakeResponse: {
493
+ status: response.status,
494
+ statusText: response.statusText,
495
+ headers
496
+ }
490
497
  })
491
498
  }
492
499
  }
@@ -728,7 +735,7 @@ webidl.converters.WebSocketSendData = function (V) {
728
735
  return V
729
736
  }
730
737
 
731
- if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
738
+ if (ArrayBuffer.isView(V) || isArrayBuffer(V)) {
732
739
  return V
733
740
  }
734
741
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.13.0",
3
+ "version": "7.14.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": {
@@ -109,23 +109,23 @@
109
109
  "devDependencies": {
110
110
  "@fastify/busboy": "3.1.1",
111
111
  "@matteo.collina/tspl": "^0.2.0",
112
+ "@metcoder95/https-pem": "^1.0.0",
112
113
  "@sinonjs/fake-timers": "^12.0.0",
113
114
  "@types/node": "^18.19.50",
114
115
  "abort-controller": "^3.0.0",
115
116
  "borp": "^0.20.0",
116
117
  "c8": "^10.0.0",
117
- "cross-env": "^7.0.3",
118
+ "cross-env": "^10.0.0",
118
119
  "dns-packet": "^5.4.0",
119
120
  "esbuild": "^0.25.2",
120
121
  "eslint": "^9.9.0",
121
122
  "fast-check": "^4.1.1",
122
- "https-pem": "^3.0.0",
123
123
  "husky": "^9.0.7",
124
- "jest": "^29.0.2",
124
+ "jest": "^30.0.5",
125
125
  "neostandard": "^0.12.0",
126
126
  "node-forge": "^1.3.1",
127
127
  "proxy": "^2.1.1",
128
- "tsd": "^0.32.0",
128
+ "tsd": "^0.33.0",
129
129
  "typescript": "^5.6.2",
130
130
  "ws": "^8.11.0"
131
131
  },
@@ -56,6 +56,11 @@ export declare const EventSource: {
56
56
  }
57
57
 
58
58
  interface EventSourceInit {
59
- withCredentials?: boolean,
59
+ withCredentials?: boolean
60
+ // @deprecated use `node.dispatcher` instead
60
61
  dispatcher?: Dispatcher
62
+ node?: {
63
+ dispatcher?: Dispatcher
64
+ reconnectionTime?: number
65
+ }
61
66
  }
package/types/index.d.ts CHANGED
@@ -34,7 +34,9 @@ export * from './content-type'
34
34
  export * from './cache'
35
35
  export { Interceptable } from './mock-interceptor'
36
36
 
37
- export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, SnapshotAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient }
37
+ declare function globalThisInstall (): void
38
+
39
+ export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, SnapshotAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient, globalThisInstall as install }
38
40
  export default Undici
39
41
 
40
42
  declare namespace Undici {
@@ -74,4 +76,5 @@ declare namespace Undici {
74
76
  MemoryCacheStore: typeof import('./cache-interceptor').default.MemoryCacheStore,
75
77
  SqliteCacheStore: typeof import('./cache-interceptor').default.SqliteCacheStore
76
78
  }
79
+ const install: typeof globalThisInstall
77
80
  }