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 +1 -1
- package/docs/docs/api/Dispatcher.md +1 -1
- package/docs/docs/api/RetryAgent.md +0 -1
- package/docs/docs/api/RetryHandler.md +0 -1
- package/lib/dispatcher/agent.js +23 -7
- package/lib/dispatcher/client.js +6 -2
- package/lib/dispatcher/env-http-proxy-agent.js +2 -24
- package/lib/dispatcher/proxy-agent.js +7 -1
- package/lib/global.js +7 -11
- package/lib/web/fetch/index.js +198 -181
- package/lib/web/websocket/stream/websocketstream.js +0 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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']`
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -72,14 +72,17 @@ class Agent extends DispatcherBase {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
[kDispatch] (opts, handler) {
|
|
75
|
-
let
|
|
75
|
+
let origin
|
|
76
76
|
if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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)
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -235,9 +235,13 @@ class Client extends DispatcherBase {
|
|
|
235
235
|
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
|
|
236
236
|
...connect
|
|
237
237
|
})
|
|
238
|
-
} else
|
|
238
|
+
} else {
|
|
239
239
|
const customConnect = connect
|
|
240
|
-
connect = (opts, callback) => customConnect({
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent)
|
|
27
|
+
const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent)
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 () {
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -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
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
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
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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
|
-
|
|
2190
|
-
const headersList = new HeadersList()
|
|
2166
|
+
const abort = (reason) => controller.abort(reason)
|
|
2191
2167
|
|
|
2192
|
-
|
|
2193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
//
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
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
|
-
}
|
|
2274
|
-
|
|
2275
|
-
})
|
|
2276
|
-
},
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2277
2265
|
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
-
|
|
2284
|
-
|
|
2287
|
+
// 1. If one or more bytes have been transmitted from response’s
|
|
2288
|
+
// message body, then:
|
|
2285
2289
|
|
|
2286
|
-
|
|
2287
|
-
|
|
2290
|
+
// 1. Let bytes be the transmitted bytes.
|
|
2291
|
+
const bytes = chunk
|
|
2288
2292
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
-
|
|
2294
|
-
|
|
2297
|
+
// 3. Increase timingInfo’s encoded body size by bytes’s length.
|
|
2298
|
+
timingInfo.encodedBodySize += bytes.byteLength
|
|
2295
2299
|
|
|
2296
|
-
|
|
2300
|
+
// 4. See pullAlgorithm...
|
|
2297
2301
|
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
+
if (this.body.push(bytes) === false) {
|
|
2303
|
+
controller.pause()
|
|
2304
|
+
}
|
|
2305
|
+
},
|
|
2302
2306
|
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
+
onResponseEnd () {
|
|
2308
|
+
if (this.abort) {
|
|
2309
|
+
fetchParams.controller.off('terminated', this.abort)
|
|
2310
|
+
}
|
|
2307
2311
|
|
|
2308
|
-
|
|
2312
|
+
fetchParams.controller.ended = true
|
|
2309
2313
|
|
|
2310
|
-
|
|
2311
|
-
|
|
2314
|
+
this.body?.push(null)
|
|
2315
|
+
},
|
|
2312
2316
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
+
onResponseError (_controller, error) {
|
|
2318
|
+
if (this.abort) {
|
|
2319
|
+
fetchParams.controller.off('terminated', this.abort)
|
|
2320
|
+
}
|
|
2317
2321
|
|
|
2318
|
-
|
|
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
|
-
|
|
2334
|
+
this.body?.destroy(error)
|
|
2321
2335
|
|
|
2322
|
-
|
|
2323
|
-
},
|
|
2336
|
+
fetchParams.controller.terminate(error)
|
|
2324
2337
|
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
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
|
-
|
|
2333
|
-
|
|
2348
|
+
const rawHeaders = controller?.rawHeaders ?? []
|
|
2349
|
+
const headersList = new HeadersList()
|
|
2334
2350
|
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
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
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2363
|
+
resolve({
|
|
2364
|
+
status,
|
|
2365
|
+
statusText: STATUS_CODES[status],
|
|
2366
|
+
headersList,
|
|
2367
|
+
socket
|
|
2368
|
+
})
|
|
2353
2369
|
|
|
2354
|
-
|
|
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
|
|