undici 8.10.1 → 8.10.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/docs/docs/api/Interceptors.md +7 -3
- package/docs/docs/api/Socks5ProxyAgent.md +7 -1
- package/lib/cache/memory-cache-store.js +1 -1
- package/lib/core/symbols.js +2 -0
- package/lib/dispatcher/balanced-pool.js +6 -3
- package/lib/dispatcher/dispatcher.js +6 -2
- package/lib/dispatcher/dispatcher1-wrapper.js +3 -0
- package/lib/dispatcher/proxy-agent.js +17 -0
- package/lib/dispatcher/retry-agent.js +3 -0
- package/lib/dispatcher/socks5-proxy-agent.js +90 -22
- package/lib/handler/cache-handler.js +21 -5
- package/lib/handler/redirect-handler.js +13 -4
- package/lib/handler/retry-handler.js +65 -19
- package/lib/interceptor/cache.js +65 -12
- package/lib/interceptor/decompress.js +115 -13
- package/lib/interceptor/deduplicate.js +5 -4
- package/lib/interceptor/dns.js +7 -0
- package/lib/interceptor/dump.js +9 -22
- package/lib/util/cache.js +45 -3
- package/lib/web/cookies/parse.js +150 -149
- package/lib/web/eventsource/eventsource.js +9 -2
- package/lib/web/fetch/index.js +6 -0
- package/lib/web/fetch/util.js +6 -1
- package/lib/web/webidl/index.js +3 -3
- package/lib/web/websocket/connection.js +1 -1
- package/lib/web/websocket/permessage-deflate.js +5 -0
- package/lib/web/websocket/stream/websocketstream.js +8 -10
- package/package.json +1 -1
- package/types/interceptors.d.ts +2 -0
- package/types/mock-agent.d.ts +11 -10
- package/types/socks5-proxy-agent.d.ts +2 -0
|
@@ -101,8 +101,8 @@ body yourself.
|
|
|
101
101
|
|
|
102
102
|
* `opts` {Object} (optional)
|
|
103
103
|
* `maxSize` {number} Maximum number of bytes to read and discard. Responses
|
|
104
|
-
whose
|
|
105
|
-
`1_048_576` (1 MiB).
|
|
104
|
+
whose declared or received body size exceeds this value are aborted.
|
|
105
|
+
**Default:** `1_048_576` (1 MiB).
|
|
106
106
|
|
|
107
107
|
Per-request override: set `dumpMaxSize` on the dispatch options to override
|
|
108
108
|
the global `maxSize` for a specific request.
|
|
@@ -210,6 +210,9 @@ Automatically decompresses response bodies encoded with `gzip`, `x-gzip`,
|
|
|
210
210
|
skipped. **Default:** `[204, 304]`.
|
|
211
211
|
* `skipErrorResponses` {boolean} When `true`, responses with a status code
|
|
212
212
|
>= 400 are not decompressed. **Default:** `true`.
|
|
213
|
+
* `maxSize` {number} Maximum decompressed response size in bytes. The request
|
|
214
|
+
fails with a `ResponseExceededMaxSizeError` if the decoded body exceeds
|
|
215
|
+
this limit. **Default:** `67108864` (64 MiB).
|
|
213
216
|
|
|
214
217
|
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
|
|
215
218
|
|
|
@@ -221,7 +224,8 @@ import { Agent, interceptors } from 'undici'
|
|
|
221
224
|
const agent = new Agent().compose(
|
|
222
225
|
interceptors.decompress({
|
|
223
226
|
skipStatusCodes: [204, 304],
|
|
224
|
-
skipErrorResponses: false // decompress error bodies too
|
|
227
|
+
skipErrorResponses: false, // decompress error bodies too
|
|
228
|
+
maxSize: 16 * 1024 * 1024 // limit decoded bodies to 16 MiB
|
|
225
229
|
})
|
|
226
230
|
)
|
|
227
231
|
```
|
|
@@ -60,6 +60,11 @@ added: v7.23.0
|
|
|
60
60
|
a password embedded in `proxyUrl`. **Default:** the URL password, if any.
|
|
61
61
|
* `connect` {Function} Custom connector used to open the socket to the proxy.
|
|
62
62
|
**Default:** a connector built from `proxyTls`.
|
|
63
|
+
* `connectTimeout` {number} Maximum time in milliseconds for each proxy
|
|
64
|
+
connection, SOCKS5 negotiation, and target TLS negotiation stage. A value of
|
|
65
|
+
`0` disables these stage timeouts. `proxyTls.timeout` and
|
|
66
|
+
`requestTls.timeout` override it for their respective TLS stages.
|
|
67
|
+
**Default:** `5000`.
|
|
63
68
|
* `proxyTls` {BuildOptions} TLS options for the connection to the proxy itself
|
|
64
69
|
(SOCKS5 over TLS). When set, the proxy connection is established over TLS and
|
|
65
70
|
`servername` defaults to the proxy host name.
|
|
@@ -69,7 +74,8 @@ added: v7.23.0
|
|
|
69
74
|
host name.
|
|
70
75
|
|
|
71
76
|
Throws an `InvalidArgumentError` if `proxyUrl` is missing or does not use the
|
|
72
|
-
`socks5:` or `socks:` protocol.
|
|
77
|
+
`socks5:` or `socks:` protocol, or if `connectTimeout`, `proxyTls.timeout`, or
|
|
78
|
+
`requestTls.timeout` is not a finite, non-negative number.
|
|
73
79
|
|
|
74
80
|
```mjs
|
|
75
81
|
import { Socks5ProxyAgent } from 'undici'
|
|
@@ -87,7 +87,7 @@ class MemoryCacheStore extends EventEmitter {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
90
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
91
91
|
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
|
|
92
92
|
*/
|
|
93
93
|
get (key) {
|
package/lib/core/symbols.js
CHANGED
|
@@ -5,6 +5,8 @@ module.exports = {
|
|
|
5
5
|
kDestroy: Symbol('destroy'),
|
|
6
6
|
kDispatch: Symbol('dispatch'),
|
|
7
7
|
kUrl: Symbol('url'),
|
|
8
|
+
kRequestOrigin: Symbol('request origin'),
|
|
9
|
+
kOriginless: Symbol('originless'),
|
|
8
10
|
kWriting: Symbol('writing'),
|
|
9
11
|
kResuming: Symbol('resuming'),
|
|
10
12
|
kQueue: Symbol('queue'),
|
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
kGetDispatcher
|
|
14
14
|
} = require('./pool-base')
|
|
15
15
|
const Pool = require('./pool')
|
|
16
|
-
const { kUrl } = require('../core/symbols')
|
|
16
|
+
const { kOriginless, kUrl } = require('../core/symbols')
|
|
17
17
|
const util = require('../core/util')
|
|
18
18
|
const kFactory = Symbol('factory')
|
|
19
19
|
|
|
@@ -49,14 +49,17 @@ function defaultFactory (origin, opts) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
class BalancedPool extends PoolBase {
|
|
52
|
-
constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
|
|
52
|
+
constructor (upstreams = [], { factory = defaultFactory, connect, tls, ...opts } = {}) {
|
|
53
53
|
if (typeof factory !== 'function') {
|
|
54
54
|
throw new InvalidArgumentError('factory must be a function.')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
super(opts)
|
|
58
58
|
|
|
59
|
-
this[
|
|
59
|
+
this[kOriginless] = true
|
|
60
|
+
if (connect && typeof connect !== 'function') connect = { ...connect }
|
|
61
|
+
if (tls && typeof tls !== 'function') tls = { ...tls }
|
|
62
|
+
this[kOptions] = { ...util.deepClone(opts), connect, tls }
|
|
60
63
|
this[kIndex] = -1
|
|
61
64
|
this[kCurrentWeight] = 0
|
|
62
65
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const EventEmitter = require('node:events')
|
|
3
|
-
const { kUrl } = require('../core/symbols')
|
|
3
|
+
const { kOriginless, kUrl } = require('../core/symbols')
|
|
4
4
|
|
|
5
5
|
class Dispatcher extends EventEmitter {
|
|
6
6
|
dispatch () {
|
|
@@ -18,6 +18,10 @@ class Dispatcher extends EventEmitter {
|
|
|
18
18
|
compose (...args) {
|
|
19
19
|
// So we handle [interceptor1, interceptor2] or interceptor1, interceptor2, ...
|
|
20
20
|
const interceptors = Array.isArray(args[0]) ? args[0] : args
|
|
21
|
+
// null disables origin-dependent interceptors; undefined uses opts.origin.
|
|
22
|
+
const interceptorOrigin = this[kOriginless] === true
|
|
23
|
+
? null
|
|
24
|
+
: this[kUrl]?.origin
|
|
21
25
|
let dispatch = this.dispatch.bind(this)
|
|
22
26
|
|
|
23
27
|
for (const interceptor of interceptors) {
|
|
@@ -29,7 +33,7 @@ class Dispatcher extends EventEmitter {
|
|
|
29
33
|
throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
dispatch = interceptor(dispatch)
|
|
36
|
+
dispatch = interceptor(dispatch, interceptorOrigin)
|
|
33
37
|
|
|
34
38
|
if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
|
|
35
39
|
throw new TypeError('invalid interceptor')
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const Dispatcher = require('./dispatcher')
|
|
4
4
|
const { InvalidArgumentError } = require('../core/errors')
|
|
5
5
|
const { toRawHeaders } = require('../core/util')
|
|
6
|
+
const { kOriginless, kUrl } = require('../core/symbols')
|
|
6
7
|
|
|
7
8
|
class LegacyHandlerWrapper {
|
|
8
9
|
#handler
|
|
@@ -71,6 +72,8 @@ class Dispatcher1Wrapper extends Dispatcher {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
this.#dispatcher = dispatcher
|
|
75
|
+
this[kUrl] = dispatcher[kUrl]
|
|
76
|
+
this[kOriginless] = dispatcher[kOriginless]
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
static wrapHandler (handler) {
|
|
@@ -9,6 +9,7 @@ const buildConnector = require('../core/connect')
|
|
|
9
9
|
const Client = require('./client')
|
|
10
10
|
const { channels } = require('../core/diagnostics')
|
|
11
11
|
const Socks5ProxyAgent = require('./socks5-proxy-agent')
|
|
12
|
+
const { hasSafeIterator } = require('../core/util')
|
|
12
13
|
|
|
13
14
|
const kAgent = Symbol('proxy agent')
|
|
14
15
|
const kClient = Symbol('proxy client')
|
|
@@ -161,6 +162,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
161
162
|
factory: agentFactory,
|
|
162
163
|
username: opts.username || username,
|
|
163
164
|
password: opts.password || password,
|
|
165
|
+
connectTimeout,
|
|
164
166
|
proxyTls: opts.proxyTls,
|
|
165
167
|
requestTls: opts.requestTls
|
|
166
168
|
})
|
|
@@ -344,6 +346,21 @@ function buildHeaders (headers) {
|
|
|
344
346
|
return headersPair
|
|
345
347
|
}
|
|
346
348
|
|
|
349
|
+
// Materialize iterable header containers (e.g. Map, Headers) into a record so
|
|
350
|
+
// that throwIfProxyAuthIsSent() can inspect their entries. Object.keys and
|
|
351
|
+
// for...in see nothing on a Map/Headers instance, so without this the
|
|
352
|
+
// Proxy-Authorization guard is bypassed and proxy credentials can reach the
|
|
353
|
+
// origin server (GHSA-6cv7-626c-qhqw).
|
|
354
|
+
if (headers && typeof headers === 'object' && hasSafeIterator(headers)) {
|
|
355
|
+
const headersPair = {}
|
|
356
|
+
|
|
357
|
+
for (const [key, value] of headers) {
|
|
358
|
+
headersPair[key] = value
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return headersPair
|
|
362
|
+
}
|
|
363
|
+
|
|
347
364
|
return headers
|
|
348
365
|
}
|
|
349
366
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Dispatcher = require('./dispatcher')
|
|
4
4
|
const RetryHandler = require('../handler/retry-handler')
|
|
5
|
+
const { kOriginless, kUrl } = require('../core/symbols')
|
|
5
6
|
|
|
6
7
|
class RetryAgent extends Dispatcher {
|
|
7
8
|
#agent = null
|
|
@@ -10,6 +11,8 @@ class RetryAgent extends Dispatcher {
|
|
|
10
11
|
super(options)
|
|
11
12
|
this.#agent = agent
|
|
12
13
|
this.#options = options
|
|
14
|
+
this[kUrl] = agent[kUrl]
|
|
15
|
+
this[kOriginless] = agent[kOriginless]
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
dispatch (opts, handler) {
|
|
@@ -4,22 +4,33 @@ const { URL } = require('node:url')
|
|
|
4
4
|
|
|
5
5
|
let tls // include tls conditionally since it is not always available
|
|
6
6
|
const DispatcherBase = require('./dispatcher-base')
|
|
7
|
-
const { InvalidArgumentError } = require('../core/errors')
|
|
7
|
+
const { ConnectTimeoutError, InvalidArgumentError } = require('../core/errors')
|
|
8
8
|
const { Socks5Client, STATES } = require('../core/socks5-client')
|
|
9
9
|
const { kBusy, kConnected, kDispatch, kClose, kDestroy } = require('../core/symbols')
|
|
10
10
|
const Pool = require('./pool')
|
|
11
11
|
const buildConnector = require('../core/connect')
|
|
12
|
+
const { setupConnectTimeout } = require('../core/util')
|
|
12
13
|
const { debuglog } = require('node:util')
|
|
13
14
|
|
|
14
15
|
const debug = debuglog('undici:socks5-proxy')
|
|
15
16
|
|
|
17
|
+
const DEFAULT_SOCKS5_CONNECT_TIMEOUT = 5000
|
|
18
|
+
|
|
16
19
|
const kProxyUrl = Symbol('proxy url')
|
|
17
20
|
const kProxyHeaders = Symbol('proxy headers')
|
|
18
21
|
const kProxyAuth = Symbol('proxy auth')
|
|
19
22
|
const kProxyProtocol = Symbol('proxy protocol')
|
|
20
23
|
const kPools = Symbol('pools')
|
|
21
24
|
const kConnector = Symbol('connector')
|
|
25
|
+
const kConnectTimeout = Symbol('connect timeout')
|
|
22
26
|
const kRequestTls = Symbol('request tls settings')
|
|
27
|
+
const kRequestTlsTimeout = Symbol('request tls timeout')
|
|
28
|
+
|
|
29
|
+
function createConnectTimeoutError (hostname, port, timeout) {
|
|
30
|
+
return new ConnectTimeoutError(
|
|
31
|
+
`Connect Timeout Error (attempted address: ${hostname}:${port}, timeout: ${timeout}ms)`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
// Static flag to ensure warning is only emitted once per process
|
|
25
36
|
let experimentalWarningEmitted = false
|
|
@@ -54,7 +65,20 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
54
65
|
this[kProxyUrl] = url
|
|
55
66
|
this[kProxyHeaders] = options.headers || {}
|
|
56
67
|
this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:'
|
|
57
|
-
|
|
68
|
+
|
|
69
|
+
const connectTimeout = options.connectTimeout ?? DEFAULT_SOCKS5_CONNECT_TIMEOUT
|
|
70
|
+
if (!Number.isFinite(connectTimeout) || connectTimeout < 0) {
|
|
71
|
+
throw new InvalidArgumentError('invalid connectTimeout')
|
|
72
|
+
}
|
|
73
|
+
this[kConnectTimeout] = connectTimeout
|
|
74
|
+
|
|
75
|
+
const { timeout, ...requestTls } = options.requestTls || {}
|
|
76
|
+
const requestTlsTimeout = timeout ?? connectTimeout
|
|
77
|
+
if (!Number.isFinite(requestTlsTimeout) || requestTlsTimeout < 0) {
|
|
78
|
+
throw new InvalidArgumentError('invalid requestTls.timeout')
|
|
79
|
+
}
|
|
80
|
+
this[kRequestTls] = requestTls
|
|
81
|
+
this[kRequestTlsTimeout] = requestTlsTimeout
|
|
58
82
|
|
|
59
83
|
// Extract auth from URL or options
|
|
60
84
|
this[kProxyAuth] = {
|
|
@@ -63,8 +87,13 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
// Create connector for proxy connection
|
|
90
|
+
const proxyTlsTimeout = options.proxyTls?.timeout ?? connectTimeout
|
|
91
|
+
if (!Number.isFinite(proxyTlsTimeout) || proxyTlsTimeout < 0) {
|
|
92
|
+
throw new InvalidArgumentError('invalid proxyTls.timeout')
|
|
93
|
+
}
|
|
66
94
|
this[kConnector] = options.connect || buildConnector({
|
|
67
95
|
...options.proxyTls,
|
|
96
|
+
timeout: proxyTlsTimeout,
|
|
68
97
|
servername: options.proxyTls?.servername || url.hostname
|
|
69
98
|
})
|
|
70
99
|
|
|
@@ -113,21 +142,29 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
113
142
|
|
|
114
143
|
// Wait for authentication (if required)
|
|
115
144
|
const authenticationReady = Promise.withResolvers()
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
const authenticationTimeout = this[kConnectTimeout] === 0
|
|
146
|
+
? null
|
|
147
|
+
: setTimeout(() => {
|
|
148
|
+
cleanupAuthenticationListeners()
|
|
149
|
+
socks5Client.destroy()
|
|
150
|
+
authenticationReady.reject(
|
|
151
|
+
createConnectTimeoutError(proxyHost, proxyPort, this[kConnectTimeout])
|
|
152
|
+
)
|
|
153
|
+
}, this[kConnectTimeout])
|
|
154
|
+
|
|
155
|
+
const cleanupAuthenticationListeners = () => {
|
|
123
156
|
clearTimeout(authenticationTimeout)
|
|
157
|
+
socks5Client.removeListener('authenticated', onAuthenticated)
|
|
124
158
|
socks5Client.removeListener('error', onAuthenticationError)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const onAuthenticated = () => {
|
|
162
|
+
cleanupAuthenticationListeners()
|
|
125
163
|
authenticationReady.resolve()
|
|
126
164
|
}
|
|
127
165
|
|
|
128
166
|
const onAuthenticationError = (err) => {
|
|
129
|
-
|
|
130
|
-
socks5Client.removeListener('authenticated', onAuthenticated)
|
|
167
|
+
cleanupAuthenticationListeners()
|
|
131
168
|
authenticationReady.reject(err)
|
|
132
169
|
}
|
|
133
170
|
|
|
@@ -147,22 +184,30 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
147
184
|
|
|
148
185
|
// Wait for connection
|
|
149
186
|
const connectionReady = Promise.withResolvers()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
187
|
+
const connectionTimeout = this[kConnectTimeout] === 0
|
|
188
|
+
? null
|
|
189
|
+
: setTimeout(() => {
|
|
190
|
+
cleanupConnectionListeners()
|
|
191
|
+
socks5Client.destroy()
|
|
192
|
+
connectionReady.reject(
|
|
193
|
+
createConnectTimeoutError(targetHost, targetPort, this[kConnectTimeout])
|
|
194
|
+
)
|
|
195
|
+
}, this[kConnectTimeout])
|
|
196
|
+
|
|
197
|
+
const cleanupConnectionListeners = () => {
|
|
198
|
+
clearTimeout(connectionTimeout)
|
|
199
|
+
socks5Client.removeListener('connected', onConnected)
|
|
200
|
+
socks5Client.removeListener('error', onConnectionError)
|
|
201
|
+
}
|
|
155
202
|
|
|
156
203
|
const onConnected = (info) => {
|
|
157
204
|
debug('SOCKS5 tunnel established to', targetHost, targetPort, 'via', info)
|
|
158
|
-
|
|
159
|
-
socks5Client.removeListener('error', onConnectionError)
|
|
205
|
+
cleanupConnectionListeners()
|
|
160
206
|
connectionReady.resolve()
|
|
161
207
|
}
|
|
162
208
|
|
|
163
209
|
const onConnectionError = (err) => {
|
|
164
|
-
|
|
165
|
-
socks5Client.removeListener('connected', onConnected)
|
|
210
|
+
cleanupConnectionListeners()
|
|
166
211
|
connectionReady.reject(err)
|
|
167
212
|
}
|
|
168
213
|
|
|
@@ -215,8 +260,31 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
215
260
|
})
|
|
216
261
|
|
|
217
262
|
const tlsReady = Promise.withResolvers()
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
|
|
264
|
+
const cleanupTlsListeners = () => {
|
|
265
|
+
queueMicrotask(clearTlsTimeout)
|
|
266
|
+
finalSocket.removeListener('secureConnect', onSecureConnect)
|
|
267
|
+
finalSocket.removeListener('error', onTlsError)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const onSecureConnect = () => {
|
|
271
|
+
cleanupTlsListeners()
|
|
272
|
+
tlsReady.resolve()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const onTlsError = (err) => {
|
|
276
|
+
cleanupTlsListeners()
|
|
277
|
+
tlsReady.reject(err)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const clearTlsTimeout = setupConnectTimeout(new WeakRef(finalSocket), {
|
|
281
|
+
timeout: this[kRequestTlsTimeout],
|
|
282
|
+
hostname: targetHost,
|
|
283
|
+
port: targetPort
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
finalSocket.once('secureConnect', onSecureConnect)
|
|
287
|
+
finalSocket.once('error', onTlsError)
|
|
220
288
|
await tlsReady.promise
|
|
221
289
|
}
|
|
222
290
|
|
|
@@ -218,6 +218,13 @@ class CacheHandler {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const cacheControlHeader = resHeaders['cache-control']
|
|
221
|
+
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
222
|
+
|
|
223
|
+
if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) {
|
|
224
|
+
deleteCachedValue(this.#store, this.#cacheKey)
|
|
225
|
+
return downstreamOnHeaders()
|
|
226
|
+
}
|
|
227
|
+
|
|
221
228
|
const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
|
|
222
229
|
if (
|
|
223
230
|
!cacheControlHeader &&
|
|
@@ -234,8 +241,7 @@ class CacheHandler {
|
|
|
234
241
|
return downstreamOnHeaders()
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
|
|
244
|
+
if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
|
|
239
245
|
if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
|
|
240
246
|
deleteCachedValue(this.#store, this.#cacheKey)
|
|
241
247
|
}
|
|
@@ -484,7 +490,10 @@ function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
|
|
|
484
490
|
*/
|
|
485
491
|
function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
|
|
486
492
|
return cacheControlDirectives['no-store'] === true ||
|
|
487
|
-
(cacheType === 'shared' &&
|
|
493
|
+
(cacheType === 'shared' && (
|
|
494
|
+
cacheControlDirectives.private === true ||
|
|
495
|
+
Object.hasOwn(resHeaders, 'set-cookie')
|
|
496
|
+
)) ||
|
|
488
497
|
(resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
|
|
489
498
|
}
|
|
490
499
|
|
|
@@ -492,12 +501,16 @@ function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheC
|
|
|
492
501
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
493
502
|
*
|
|
494
503
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
504
|
+
* @param {string} method
|
|
495
505
|
* @param {number} statusCode
|
|
496
506
|
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
497
507
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
498
508
|
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
|
|
499
509
|
*/
|
|
500
|
-
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
|
|
510
|
+
function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
|
|
511
|
+
if (!arrayIncludes(util.safeHTTPMethods, method)) {
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
501
514
|
// Status code must be final and understood.
|
|
502
515
|
if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
|
|
503
516
|
return false
|
|
@@ -518,7 +531,10 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
|
|
|
518
531
|
return false
|
|
519
532
|
}
|
|
520
533
|
|
|
521
|
-
if (cacheType === 'shared' &&
|
|
534
|
+
if (cacheType === 'shared' && (
|
|
535
|
+
cacheControlDirectives.private === true ||
|
|
536
|
+
Object.hasOwn(resHeaders, 'set-cookie')
|
|
537
|
+
)) {
|
|
522
538
|
return false
|
|
523
539
|
}
|
|
524
540
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const { InvalidArgumentError } = require('../core/errors')
|
|
6
|
+
const { kRequestOrigin } = require('../core/symbols')
|
|
6
7
|
|
|
7
8
|
const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
|
|
8
9
|
|
|
@@ -89,8 +90,12 @@ class RedirectHandler {
|
|
|
89
90
|
? null
|
|
90
91
|
: headers.location
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
const requestOrigin = this.opts[kRequestOrigin] === undefined
|
|
94
|
+
? this.opts.origin
|
|
95
|
+
: this.opts[kRequestOrigin]
|
|
96
|
+
|
|
97
|
+
if (requestOrigin) {
|
|
98
|
+
this.history.push(new URL(this.opts.path, requestOrigin))
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
if (!this.location) {
|
|
@@ -98,7 +103,10 @@ class RedirectHandler {
|
|
|
98
103
|
return
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
const
|
|
106
|
+
const baseUrl = requestOrigin
|
|
107
|
+
? new URL(this.opts.path, requestOrigin)
|
|
108
|
+
: undefined
|
|
109
|
+
const { origin, pathname, search } = util.parseURL(new URL(this.location, baseUrl))
|
|
102
110
|
const path = search ? `${pathname}${search}` : pathname
|
|
103
111
|
|
|
104
112
|
// Check for redirect loops by seeing if we've already visited this URL in our history
|
|
@@ -114,9 +122,10 @@ class RedirectHandler {
|
|
|
114
122
|
// Remove headers referring to the original URL.
|
|
115
123
|
// By default it is Host only. A 303 or a 301/302 POST-to-GET redirect also removes all Content-* headers.
|
|
116
124
|
// https://tools.ietf.org/html/rfc7231#section-6.4
|
|
117
|
-
this.opts.headers = cleanRequestHeaders(this.opts.headers, removeContentHeaders,
|
|
125
|
+
this.opts.headers = cleanRequestHeaders(this.opts.headers, removeContentHeaders, requestOrigin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect)
|
|
118
126
|
this.opts.path = path
|
|
119
127
|
this.opts.origin = origin
|
|
128
|
+
this.opts[kRequestOrigin] = origin
|
|
120
129
|
this.opts.query = null
|
|
121
130
|
}
|
|
122
131
|
|
|
@@ -65,7 +65,18 @@ class RetryController {
|
|
|
65
65
|
get aborted () { return this.target?.aborted ?? false }
|
|
66
66
|
get reason () { return this.target?.reason ?? null }
|
|
67
67
|
get rawHeaders () { return this.target?.rawHeaders ?? null }
|
|
68
|
+
set rawHeaders (value) {
|
|
69
|
+
if (this.target) {
|
|
70
|
+
this.target.rawHeaders = value
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
get rawTrailers () { return this.target?.rawTrailers ?? null }
|
|
75
|
+
set rawTrailers (value) {
|
|
76
|
+
if (this.target) {
|
|
77
|
+
this.target.rawTrailers = value
|
|
78
|
+
}
|
|
79
|
+
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
class RetryHandler {
|
|
@@ -140,8 +151,16 @@ class RetryHandler {
|
|
|
140
151
|
if (this.retryOpts.throwOnError) {
|
|
141
152
|
// Preserve old behavior for status codes that are not eligible for retry
|
|
142
153
|
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
|
143
|
-
this.headersSent
|
|
144
|
-
|
|
154
|
+
if (this.headersSent) {
|
|
155
|
+
// The downstream handler already received the response from an
|
|
156
|
+
// earlier attempt. Forwarding this response would replace the
|
|
157
|
+
// downstream body and leave the original body pending forever.
|
|
158
|
+
this.handler.onResponseError?.(this.controllerProxy, err)
|
|
159
|
+
} else {
|
|
160
|
+
this.headersSent = true
|
|
161
|
+
this.checkpointResponseEnd(headers)
|
|
162
|
+
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
163
|
+
}
|
|
145
164
|
} else {
|
|
146
165
|
this.error = err
|
|
147
166
|
}
|
|
@@ -151,6 +170,7 @@ class RetryHandler {
|
|
|
151
170
|
|
|
152
171
|
if (isDisturbed(this.opts.body)) {
|
|
153
172
|
this.headersSent = true
|
|
173
|
+
this.checkpointResponseEnd(headers)
|
|
154
174
|
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
155
175
|
return
|
|
156
176
|
}
|
|
@@ -164,8 +184,16 @@ class RetryHandler {
|
|
|
164
184
|
this.retryTimer = null
|
|
165
185
|
|
|
166
186
|
if (passedErr) {
|
|
167
|
-
this.headersSent
|
|
168
|
-
|
|
187
|
+
if (this.headersSent) {
|
|
188
|
+
// The downstream handler already received the response from an
|
|
189
|
+
// earlier attempt. Forwarding this response would replace the
|
|
190
|
+
// downstream body and leave the original body pending forever.
|
|
191
|
+
this.handler.onResponseError?.(this.controllerProxy, passedErr)
|
|
192
|
+
} else {
|
|
193
|
+
this.headersSent = true
|
|
194
|
+
this.checkpointResponseEnd(headers)
|
|
195
|
+
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
196
|
+
}
|
|
169
197
|
controller.resume()
|
|
170
198
|
return
|
|
171
199
|
}
|
|
@@ -195,6 +223,20 @@ class RetryHandler {
|
|
|
195
223
|
) ?? null
|
|
196
224
|
}
|
|
197
225
|
|
|
226
|
+
checkpointResponseEnd (headers) {
|
|
227
|
+
if (this.end == null && this.opts.method !== 'HEAD') {
|
|
228
|
+
const contentLength = headers['content-length']
|
|
229
|
+
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
230
|
+
|
|
231
|
+
assert(
|
|
232
|
+
this.end == null || Number.isFinite(this.end),
|
|
233
|
+
'invalid content-length'
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
this.resume = this.end != null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
198
240
|
onRequestStart (controller, context) {
|
|
199
241
|
// request.js creates a fresh RequestController per dispatch and passes that
|
|
200
242
|
// same instance to every later callback of the dispatch. onRequestStart is
|
|
@@ -293,18 +335,6 @@ class RetryHandler {
|
|
|
293
335
|
this.statusCode = statusCode
|
|
294
336
|
this.headers = headers
|
|
295
337
|
|
|
296
|
-
if (statusCode >= 300) {
|
|
297
|
-
const err = new RequestRetryError('Request failed', statusCode, {
|
|
298
|
-
headers,
|
|
299
|
-
data: {
|
|
300
|
-
count: this.retryCount
|
|
301
|
-
}
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
338
|
// Checkpoint for resume from where we left it
|
|
309
339
|
if (this.headersSent) {
|
|
310
340
|
// Only Partial Content 206 supposed to provide Content-Range,
|
|
@@ -341,12 +371,28 @@ class RetryHandler {
|
|
|
341
371
|
|
|
342
372
|
const { start, size, end = size ? size - 1 : null } = contentRange
|
|
343
373
|
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
if (this.start !== start || (this.end != null && this.end !== end)) {
|
|
375
|
+
throw new RequestRetryError('Content-Range mismatch', statusCode, {
|
|
376
|
+
headers,
|
|
377
|
+
data: { count: this.retryCount }
|
|
378
|
+
})
|
|
379
|
+
}
|
|
346
380
|
|
|
347
381
|
return
|
|
348
382
|
}
|
|
349
383
|
|
|
384
|
+
if (statusCode >= 300) {
|
|
385
|
+
const err = new RequestRetryError('Request failed', statusCode, {
|
|
386
|
+
headers,
|
|
387
|
+
data: {
|
|
388
|
+
count: this.retryCount
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
350
396
|
if (this.end == null) {
|
|
351
397
|
if (statusCode === 206) {
|
|
352
398
|
// First time we receive 206
|
|
@@ -485,7 +531,7 @@ class RetryHandler {
|
|
|
485
531
|
|
|
486
532
|
// controller is THIS failed connection (not the proxy): we inspect whether
|
|
487
533
|
// the consumer aborted it to decide retry-vs-propagate.
|
|
488
|
-
if (controller?.aborted || isDisturbed(this.opts.body)) {
|
|
534
|
+
if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) {
|
|
489
535
|
this.handler.onResponseError?.(this.controllerProxy, err)
|
|
490
536
|
return
|
|
491
537
|
}
|