undici 8.0.0 → 8.0.2

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
@@ -621,7 +621,7 @@ including undici that is bundled internally with node.js.
621
621
 
622
622
  Undici stores this dispatcher under `Symbol.for('undici.globalDispatcher.2')`.
623
623
 
624
- On Node.js 22, `setGlobalDispatcher()` also mirrors the configured dispatcher to
624
+ `setGlobalDispatcher()` also mirrors the configured dispatcher to
625
625
  `Symbol.for('undici.globalDispatcher.1')` using `Dispatcher1Wrapper`, so Node.js built-in `fetch`
626
626
  can keep using the legacy handler contract while Undici uses the new handler API.
627
627
 
@@ -237,7 +237,7 @@ Pause/resume now uses the controller:
237
237
  Undici now stores the global dispatcher under `Symbol.for('undici.globalDispatcher.2')`.
238
238
  This avoids conflicts with runtimes (such as Node.js built-in `fetch`) that still rely on the legacy dispatcher handler interface.
239
239
 
240
- On Node.js 22, `setGlobalDispatcher()` also mirrors the configured dispatcher to `Symbol.for('undici.globalDispatcher.1')` using a `Dispatcher1Wrapper`, so Node's built-in `fetch` can keep using the legacy handler contract.
240
+ `setGlobalDispatcher()` also mirrors the configured dispatcher to `Symbol.for('undici.globalDispatcher.1')` using a `Dispatcher1Wrapper`, so Node's built-in `fetch` can keep using the legacy handler contract.
241
241
 
242
242
  If you need to expose a new dispatcher/agent to legacy v1 handler consumers (`onConnect/onHeaders/onData/onComplete/onError/onUpgrade`), use `Dispatcher1Wrapper`:
243
243
 
@@ -23,7 +23,6 @@ Returns: `ProxyAgent`
23
23
  - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
24
24
  - **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
25
25
  - **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
26
- -
27
26
  - **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
28
27
  - **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
29
28
  - **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']`
@@ -26,7 +26,6 @@ Extends: [`Dispatch.DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dis
26
26
  - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
27
27
  - **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
28
28
  - **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
29
- -
30
29
  - **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
31
30
  - **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
32
31
  - **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']`
@@ -72,14 +72,17 @@ class Agent extends DispatcherBase {
72
72
  }
73
73
 
74
74
  [kDispatch] (opts, handler) {
75
- let key
75
+ let origin
76
76
  if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
77
- key = String(opts.origin)
77
+ origin = String(opts.origin)
78
78
  } else {
79
79
  throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
80
80
  }
81
81
 
82
- if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) {
82
+ const allowH2 = opts.allowH2 ?? this[kOptions].allowH2
83
+ const key = allowH2 === false ? `${origin}#http1-only` : origin
84
+
85
+ if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(origin)) {
83
86
  throw new MaxOriginsReachedError()
84
87
  }
85
88
 
@@ -96,10 +99,23 @@ class Agent extends DispatcherBase {
96
99
  result.dispatcher.close()
97
100
  }
98
101
  }
99
- this[kOrigins].delete(key)
102
+
103
+ let hasOrigin = false
104
+ for (const entry of this[kClients].values()) {
105
+ if (entry.origin === origin) {
106
+ hasOrigin = true
107
+ break
108
+ }
109
+ }
110
+
111
+ if (!hasOrigin) {
112
+ this[kOrigins].delete(origin)
113
+ }
100
114
  }
101
115
  }
102
- dispatcher = this[kFactory](opts.origin, this[kOptions])
116
+ dispatcher = this[kFactory](opts.origin, allowH2 === false
117
+ ? { ...this[kOptions], allowH2: false }
118
+ : this[kOptions])
103
119
  .on('drain', this[kOnDrain])
104
120
  .on('connect', (origin, targets) => {
105
121
  const result = this[kClients].get(key)
@@ -117,8 +133,8 @@ class Agent extends DispatcherBase {
117
133
  this[kOnConnectionError](origin, targets, err)
118
134
  })
119
135
 
120
- this[kClients].set(key, { count: 0, dispatcher })
121
- this[kOrigins].add(key)
136
+ this[kClients].set(key, { count: 0, dispatcher, origin })
137
+ this[kOrigins].add(origin)
122
138
  }
123
139
 
124
140
  return dispatcher.dispatch(opts, handler)
@@ -235,9 +235,13 @@ class Client extends DispatcherBase {
235
235
  ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
236
236
  ...connect
237
237
  })
238
- } else if (socketPath != null) {
238
+ } else {
239
239
  const customConnect = connect
240
- connect = (opts, callback) => customConnect({ ...opts, socketPath }, callback)
240
+ connect = (opts, callback) => customConnect({
241
+ ...opts,
242
+ ...(socketPath != null ? { socketPath } : null),
243
+ ...(allowH2 != null ? { allowH2 } : null)
244
+ }, callback)
241
245
  }
242
246
 
243
247
  this[kUrl] = util.parseOrigin(url)
@@ -10,22 +10,6 @@ const DEFAULT_PORTS = {
10
10
  'https:': 443
11
11
  }
12
12
 
13
- /**
14
- * Normalizes a proxy URL by prepending a scheme if one is missing.
15
- * This matches the behavior of curl and Go's httpproxy package, which
16
- * assume http:// for scheme-less proxy values.
17
- *
18
- * @param {string} proxyUrl - The proxy URL to normalize
19
- * @param {string} defaultScheme - The scheme to prepend if missing ('http' or 'https')
20
- * @returns {string} The normalized proxy URL
21
- */
22
- function normalizeProxyUrl (proxyUrl, defaultScheme) {
23
- if (!proxyUrl) return proxyUrl
24
- // If the value already contains a scheme (e.g. http://, https://, socks5://), return as-is
25
- if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(proxyUrl)) return proxyUrl
26
- return `${defaultScheme}://${proxyUrl}`
27
- }
28
-
29
13
  class EnvHttpProxyAgent extends DispatcherBase {
30
14
  #noProxyValue = null
31
15
  #noProxyEntries = null
@@ -39,20 +23,14 @@ class EnvHttpProxyAgent extends DispatcherBase {
39
23
 
40
24
  this[kNoProxyAgent] = new Agent(agentOpts)
41
25
 
42
- const HTTP_PROXY = normalizeProxyUrl(
43
- httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY,
44
- 'http'
45
- )
26
+ const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY
46
27
  if (HTTP_PROXY) {
47
28
  this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY })
48
29
  } else {
49
30
  this[kHttpProxyAgent] = this[kNoProxyAgent]
50
31
  }
51
32
 
52
- const HTTPS_PROXY = normalizeProxyUrl(
53
- httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY,
54
- 'https'
55
- )
33
+ const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY
56
34
  if (HTTPS_PROXY) {
57
35
  this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY })
58
36
  } else {
@@ -16,6 +16,7 @@ const kProxyHeaders = Symbol('proxy headers')
16
16
  const kRequestTls = Symbol('request tls settings')
17
17
  const kProxyTls = Symbol('proxy tls settings')
18
18
  const kConnectEndpoint = Symbol('connect endpoint function')
19
+ const kConnectEndpointHTTP1 = Symbol('connect endpoint function (http/1.1 only)')
19
20
  const kTunnelProxy = Symbol('tunnel proxy')
20
21
 
21
22
  function defaultProtocolPort (protocol) {
@@ -129,6 +130,7 @@ class ProxyAgent extends DispatcherBase {
129
130
 
130
131
  const connect = buildConnector({ ...opts.proxyTls })
131
132
  this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
133
+ this[kConnectEndpointHTTP1] = buildConnector({ ...opts.requestTls, allowH2: false })
132
134
 
133
135
  const agentFactory = opts.factory || defaultAgentFactory
134
136
  const factory = (origin, options) => {
@@ -216,7 +218,11 @@ class ProxyAgent extends DispatcherBase {
216
218
  } else {
217
219
  servername = opts.servername
218
220
  }
219
- this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
221
+ const connectEndpoint = opts.allowH2 === false
222
+ ? this[kConnectEndpointHTTP1]
223
+ : this[kConnectEndpoint]
224
+
225
+ connectEndpoint({ ...opts, servername, httpSocket: socket }, callback)
220
226
  } catch (err) {
221
227
  if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
222
228
  // Throw a custom error to avoid loop in client.js#connect
package/lib/global.js CHANGED
@@ -8,8 +8,6 @@ const { InvalidArgumentError } = require('./core/errors')
8
8
  const Agent = require('./dispatcher/agent')
9
9
  const Dispatcher1Wrapper = require('./dispatcher/dispatcher1-wrapper')
10
10
 
11
- const nodeMajor = Number(process.versions.node.split('.', 1)[0])
12
-
13
11
  if (getGlobalDispatcher() === undefined) {
14
12
  setGlobalDispatcher(new Agent())
15
13
  }
@@ -26,16 +24,14 @@ function setGlobalDispatcher (agent) {
26
24
  configurable: false
27
25
  })
28
26
 
29
- if (nodeMajor === 22) {
30
- const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent)
27
+ const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent)
31
28
 
32
- Object.defineProperty(globalThis, legacyGlobalDispatcher, {
33
- value: legacyAgent,
34
- writable: true,
35
- enumerable: false,
36
- configurable: false
37
- })
38
- }
29
+ Object.defineProperty(globalThis, legacyGlobalDispatcher, {
30
+ value: legacyAgent,
31
+ writable: true,
32
+ enumerable: false,
33
+ configurable: false
34
+ })
39
35
  }
40
36
 
41
37
  function getGlobalDispatcher () {
@@ -2135,226 +2135,243 @@ async function httpNetworkFetch (
2135
2135
  const path = url.pathname + url.search
2136
2136
  const hasTrailingQuestionMark = url.search.length === 0 && url.href[url.href.length - url.hash.length - 1] === '?'
2137
2137
 
2138
- return new Promise((resolve, reject) => agent.dispatch(
2139
- {
2140
- path: hasTrailingQuestionMark ? `${path}?` : path,
2141
- origin: url.origin,
2142
- method: request.method,
2143
- body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
2144
- headers: request.headersList.entries,
2145
- maxRedirections: 0,
2146
- upgrade: request.mode === 'websocket' ? 'websocket' : undefined
2147
- },
2148
- {
2149
- body: null,
2150
- abort: null,
2151
-
2152
- onRequestStart (controller) {
2153
- // TODO (fix): Do we need connection here?
2154
- const { connection } = fetchParams.controller
2155
-
2156
- // Set timingInfo’s final connection timing info to the result of calling clamp and coarsen
2157
- // connection timing info with connection’s timing info, timingInfo’s post-redirect start
2158
- // time, and fetchParams’s cross-origin isolated capability.
2159
- // TODO: implement connection timing
2160
- timingInfo.finalConnectionTimingInfo = clampAndCoarsenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)
2161
-
2162
- const abort = (reason) => controller.abort(reason)
2163
-
2164
- if (connection.destroyed) {
2165
- abort(new DOMException('The operation was aborted.', 'AbortError'))
2166
- } else {
2167
- fetchParams.controller.on('terminated', abort)
2168
- this.abort = connection.abort = abort
2169
- }
2170
-
2171
- // Set timingInfo’s final network-request start time to the coarsened shared current time given
2172
- // fetchParams’s cross-origin isolated capability.
2173
- timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
2138
+ return dispatchWithProtocolPreference(body)
2139
+
2140
+ function dispatchWithProtocolPreference (body, allowH2) {
2141
+ return new Promise((resolve, reject) => agent.dispatch(
2142
+ {
2143
+ path: hasTrailingQuestionMark ? `${path}?` : path,
2144
+ origin: url.origin,
2145
+ method: request.method,
2146
+ body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
2147
+ headers: request.headersList.entries,
2148
+ maxRedirections: 0,
2149
+ upgrade: request.mode === 'websocket' ? 'websocket' : undefined,
2150
+ ...(allowH2 === false ? { allowH2 } : null)
2174
2151
  },
2152
+ {
2153
+ body: null,
2154
+ abort: null,
2175
2155
 
2176
- onResponseStarted () {
2177
- // Set timingInfo’s final network-response start time to the coarsened shared current
2178
- // time given fetchParams’s cross-origin isolated capability, immediately after the
2179
- // user agent’s HTTP parser receives the first byte of the response (e.g., frame header
2180
- // bytes for HTTP/2 or response status line for HTTP/1.x).
2181
- timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
2182
- },
2156
+ onRequestStart (controller) {
2157
+ // TODO (fix): Do we need connection here?
2158
+ const { connection } = fetchParams.controller
2183
2159
 
2184
- onResponseStart (controller, status, _headers, statusText) {
2185
- if (status < 200) {
2186
- return
2187
- }
2160
+ // Set timingInfo’s final connection timing info to the result of calling clamp and coarsen
2161
+ // connection timing info with connection’s timing info, timingInfo’s post-redirect start
2162
+ // time, and fetchParams’s cross-origin isolated capability.
2163
+ // TODO: implement connection timing
2164
+ timingInfo.finalConnectionTimingInfo = clampAndCoarsenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)
2188
2165
 
2189
- const rawHeaders = controller?.rawHeaders ?? []
2190
- const headersList = new HeadersList()
2166
+ const abort = (reason) => controller.abort(reason)
2191
2167
 
2192
- for (let i = 0; i < rawHeaders.length; i += 2) {
2193
- const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
2194
- const value = rawHeaders[i + 1]
2195
- if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
2196
- for (const val of value) {
2197
- headersList.append(nameStr, val.toString('latin1'), true)
2198
- }
2168
+ if (connection.destroyed) {
2169
+ abort(new DOMException('The operation was aborted.', 'AbortError'))
2199
2170
  } else {
2200
- headersList.append(nameStr, value.toString('latin1'), true)
2171
+ fetchParams.controller.on('terminated', abort)
2172
+ this.abort = connection.abort = abort
2201
2173
  }
2202
- }
2203
- const location = headersList.get('location', true)
2204
-
2205
- this.body = new Readable({ read: () => controller.resume() })
2206
-
2207
- const willFollow = location && request.redirect === 'follow' &&
2208
- redirectStatusSet.has(status)
2209
2174
 
2210
- const decoders = []
2211
-
2212
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2213
- if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2214
- // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2215
- const contentEncoding = headersList.get('content-encoding', true)
2216
- // "All content-coding values are case-insensitive..."
2217
- /** @type {string[]} */
2218
- const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
2219
-
2220
- // Limit the number of content-encodings to prevent resource exhaustion.
2221
- // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
2222
- const maxContentEncodings = 5
2223
- if (codings.length > maxContentEncodings) {
2224
- reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
2175
+ // Set timingInfo’s final network-request start time to the coarsened shared current time given
2176
+ // fetchParams’s cross-origin isolated capability.
2177
+ timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
2178
+ },
2179
+
2180
+ onResponseStarted () {
2181
+ // Set timingInfo’s final network-response start time to the coarsened shared current
2182
+ // time given fetchParams’s cross-origin isolated capability, immediately after the
2183
+ // user agent’s HTTP parser receives the first byte of the response (e.g., frame header
2184
+ // bytes for HTTP/2 or response status line for HTTP/1.x).
2185
+ timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
2186
+ },
2187
+
2188
+ onResponseStart (controller, status, _headers, statusText) {
2189
+ if (status < 200) {
2225
2190
  return
2226
2191
  }
2227
2192
 
2228
- for (let i = codings.length - 1; i >= 0; --i) {
2229
- const coding = codings[i].trim()
2230
- // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
2231
- if (coding === 'x-gzip' || coding === 'gzip') {
2232
- decoders.push(zlib.createGunzip({
2233
- // Be less strict when decoding compressed responses, since sometimes
2234
- // servers send slightly invalid responses that are still accepted
2235
- // by common browsers.
2236
- // Always using Z_SYNC_FLUSH is what cURL does.
2237
- flush: zlib.constants.Z_SYNC_FLUSH,
2238
- finishFlush: zlib.constants.Z_SYNC_FLUSH
2239
- }))
2240
- } else if (coding === 'deflate') {
2241
- decoders.push(createInflate({
2242
- flush: zlib.constants.Z_SYNC_FLUSH,
2243
- finishFlush: zlib.constants.Z_SYNC_FLUSH
2244
- }))
2245
- } else if (coding === 'br') {
2246
- decoders.push(zlib.createBrotliDecompress({
2247
- flush: zlib.constants.BROTLI_OPERATION_FLUSH,
2248
- finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
2249
- }))
2250
- } else if (coding === 'zstd' && hasZstd) {
2251
- decoders.push(zlib.createZstdDecompress({
2252
- flush: zlib.constants.ZSTD_e_continue,
2253
- finishFlush: zlib.constants.ZSTD_e_end
2254
- }))
2193
+ const rawHeaders = controller?.rawHeaders ?? []
2194
+ const headersList = new HeadersList()
2195
+
2196
+ for (let i = 0; i < rawHeaders.length; i += 2) {
2197
+ const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
2198
+ const value = rawHeaders[i + 1]
2199
+ if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
2200
+ for (const val of value) {
2201
+ headersList.append(nameStr, val.toString('latin1'), true)
2202
+ }
2255
2203
  } else {
2256
- decoders.length = 0
2257
- break
2204
+ headersList.append(nameStr, value.toString('latin1'), true)
2258
2205
  }
2259
2206
  }
2260
- }
2207
+ const location = headersList.get('location', true)
2208
+
2209
+ this.body = new Readable({ read: () => controller.resume() })
2210
+
2211
+ const willFollow = location && request.redirect === 'follow' &&
2212
+ redirectStatusSet.has(status)
2213
+
2214
+ const decoders = []
2215
+
2216
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2217
+ if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2218
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2219
+ const contentEncoding = headersList.get('content-encoding', true)
2220
+ // "All content-coding values are case-insensitive..."
2221
+ /** @type {string[]} */
2222
+ const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
2261
2223
 
2262
- const onError = (err) => this.onResponseError(controller, err)
2224
+ // Limit the number of content-encodings to prevent resource exhaustion.
2225
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
2226
+ const maxContentEncodings = 5
2227
+ if (codings.length > maxContentEncodings) {
2228
+ reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
2229
+ return
2230
+ }
2263
2231
 
2264
- resolve({
2265
- status,
2266
- statusText,
2267
- headersList,
2268
- body: decoders.length
2269
- ? pipeline(this.body, ...decoders, (err) => {
2270
- if (err) {
2271
- this.onResponseError(controller, err)
2232
+ for (let i = codings.length - 1; i >= 0; --i) {
2233
+ const coding = codings[i].trim()
2234
+ // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
2235
+ if (coding === 'x-gzip' || coding === 'gzip') {
2236
+ decoders.push(zlib.createGunzip({
2237
+ // Be less strict when decoding compressed responses, since sometimes
2238
+ // servers send slightly invalid responses that are still accepted
2239
+ // by common browsers.
2240
+ // Always using Z_SYNC_FLUSH is what cURL does.
2241
+ flush: zlib.constants.Z_SYNC_FLUSH,
2242
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
2243
+ }))
2244
+ } else if (coding === 'deflate') {
2245
+ decoders.push(createInflate({
2246
+ flush: zlib.constants.Z_SYNC_FLUSH,
2247
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
2248
+ }))
2249
+ } else if (coding === 'br') {
2250
+ decoders.push(zlib.createBrotliDecompress({
2251
+ flush: zlib.constants.BROTLI_OPERATION_FLUSH,
2252
+ finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
2253
+ }))
2254
+ } else if (coding === 'zstd' && hasZstd) {
2255
+ decoders.push(zlib.createZstdDecompress({
2256
+ flush: zlib.constants.ZSTD_e_continue,
2257
+ finishFlush: zlib.constants.ZSTD_e_end
2258
+ }))
2259
+ } else {
2260
+ decoders.length = 0
2261
+ break
2272
2262
  }
2273
- }).on('error', onError)
2274
- : this.body.on('error', onError)
2275
- })
2276
- },
2263
+ }
2264
+ }
2277
2265
 
2278
- onResponseData (controller, chunk) {
2279
- if (fetchParams.controller.dump) {
2280
- return
2281
- }
2266
+ const onError = (err) => this.onResponseError(controller, err)
2267
+
2268
+ resolve({
2269
+ status,
2270
+ statusText,
2271
+ headersList,
2272
+ body: decoders.length
2273
+ ? pipeline(this.body, ...decoders, (err) => {
2274
+ if (err) {
2275
+ this.onResponseError(controller, err)
2276
+ }
2277
+ }).on('error', onError)
2278
+ : this.body.on('error', onError)
2279
+ })
2280
+ },
2281
+
2282
+ onResponseData (controller, chunk) {
2283
+ if (fetchParams.controller.dump) {
2284
+ return
2285
+ }
2282
2286
 
2283
- // 1. If one or more bytes have been transmitted from response’s
2284
- // message body, then:
2287
+ // 1. If one or more bytes have been transmitted from response’s
2288
+ // message body, then:
2285
2289
 
2286
- // 1. Let bytes be the transmitted bytes.
2287
- const bytes = chunk
2290
+ // 1. Let bytes be the transmitted bytes.
2291
+ const bytes = chunk
2288
2292
 
2289
- // 2. Let codings be the result of extracting header list values
2290
- // given `Content-Encoding` and response’s header list.
2291
- // See pullAlgorithm.
2293
+ // 2. Let codings be the result of extracting header list values
2294
+ // given `Content-Encoding` and response’s header list.
2295
+ // See pullAlgorithm.
2292
2296
 
2293
- // 3. Increase timingInfo’s encoded body size by bytes’s length.
2294
- timingInfo.encodedBodySize += bytes.byteLength
2297
+ // 3. Increase timingInfo’s encoded body size by bytes’s length.
2298
+ timingInfo.encodedBodySize += bytes.byteLength
2295
2299
 
2296
- // 4. See pullAlgorithm...
2300
+ // 4. See pullAlgorithm...
2297
2301
 
2298
- if (this.body.push(bytes) === false) {
2299
- controller.pause()
2300
- }
2301
- },
2302
+ if (this.body.push(bytes) === false) {
2303
+ controller.pause()
2304
+ }
2305
+ },
2302
2306
 
2303
- onResponseEnd () {
2304
- if (this.abort) {
2305
- fetchParams.controller.off('terminated', this.abort)
2306
- }
2307
+ onResponseEnd () {
2308
+ if (this.abort) {
2309
+ fetchParams.controller.off('terminated', this.abort)
2310
+ }
2307
2311
 
2308
- fetchParams.controller.ended = true
2312
+ fetchParams.controller.ended = true
2309
2313
 
2310
- this.body?.push(null)
2311
- },
2314
+ this.body?.push(null)
2315
+ },
2312
2316
 
2313
- onResponseError (_controller, error) {
2314
- if (this.abort) {
2315
- fetchParams.controller.off('terminated', this.abort)
2316
- }
2317
+ onResponseError (_controller, error) {
2318
+ if (this.abort) {
2319
+ fetchParams.controller.off('terminated', this.abort)
2320
+ }
2317
2321
 
2318
- this.body?.destroy(error)
2322
+ if (
2323
+ request.mode === 'websocket' &&
2324
+ allowH2 !== false &&
2325
+ error?.code === 'UND_ERR_INFO' &&
2326
+ error?.message === 'HTTP/2: Extended CONNECT protocol not supported by server'
2327
+ ) {
2328
+ // The origin negotiated H2, but RFC 8441 websocket support is unavailable.
2329
+ // Retry the opening handshake on a fresh HTTP/1.1-only connection instead.
2330
+ resolve(dispatchWithProtocolPreference(body, false))
2331
+ return
2332
+ }
2319
2333
 
2320
- fetchParams.controller.terminate(error)
2334
+ this.body?.destroy(error)
2321
2335
 
2322
- reject(error)
2323
- },
2336
+ fetchParams.controller.terminate(error)
2324
2337
 
2325
- onRequestUpgrade (controller, status, _headers, socket) {
2326
- // We need to support 200 for websocket over h2 as per RFC-8441
2327
- // Absence of session means H1
2328
- if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
2329
- return false
2330
- }
2338
+ reject(error)
2339
+ },
2340
+
2341
+ onRequestUpgrade (controller, status, _headers, socket) {
2342
+ // We need to support 200 for websocket over h2 as per RFC-8441
2343
+ // Absence of session means H1
2344
+ if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
2345
+ return false
2346
+ }
2331
2347
 
2332
- const rawHeaders = controller?.rawHeaders ?? []
2333
- const headersList = new HeadersList()
2348
+ const rawHeaders = controller?.rawHeaders ?? []
2349
+ const headersList = new HeadersList()
2334
2350
 
2335
- for (let i = 0; i < rawHeaders.length; i += 2) {
2336
- const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
2337
- const value = rawHeaders[i + 1]
2338
- if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
2339
- for (const val of value) {
2340
- headersList.append(nameStr, val.toString('latin1'), true)
2351
+ for (let i = 0; i < rawHeaders.length; i += 2) {
2352
+ const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
2353
+ const value = rawHeaders[i + 1]
2354
+ if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
2355
+ for (const val of value) {
2356
+ headersList.append(nameStr, val.toString('latin1'), true)
2357
+ }
2358
+ } else {
2359
+ headersList.append(nameStr, value.toString('latin1'), true)
2341
2360
  }
2342
- } else {
2343
- headersList.append(nameStr, value.toString('latin1'), true)
2344
2361
  }
2345
- }
2346
2362
 
2347
- resolve({
2348
- status,
2349
- statusText: STATUS_CODES[status],
2350
- headersList,
2351
- socket
2352
- })
2363
+ resolve({
2364
+ status,
2365
+ statusText: STATUS_CODES[status],
2366
+ headersList,
2367
+ socket
2368
+ })
2353
2369
 
2354
- return true
2370
+ return true
2371
+ }
2355
2372
  }
2356
- }
2357
- ))
2373
+ ))
2374
+ }
2358
2375
  }
2359
2376
  }
2360
2377
 
@@ -284,12 +284,6 @@ class WebSocketStream {
284
284
  start: (controller) => {
285
285
  this.#readableStreamController = controller
286
286
  },
287
- pull (controller) {
288
- let chunk
289
- while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
290
- controller.enqueue(chunk)
291
- }
292
- },
293
287
  cancel: (reason) => this.#cancel(reason)
294
288
  })
295
289
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "8.0.0",
3
+ "version": "8.0.2",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {