undici 7.29.1 → 7.30.1
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/Client.md +2 -0
- package/docs/docs/api/DiagnosticsChannel.md +3 -1
- package/docs/docs/api/Dispatcher.md +1 -0
- package/lib/core/connect.js +10 -4
- package/lib/core/request.js +23 -2
- package/lib/core/symbols.js +1 -0
- package/lib/dispatcher/agent.js +6 -2
- package/lib/dispatcher/client-h1.js +11 -6
- package/lib/dispatcher/client-h2.js +14 -9
- package/lib/dispatcher/client.js +3 -2
- package/lib/dispatcher/dispatcher-base.js +12 -0
- package/lib/handler/cache-handler.js +48 -0
- package/lib/handler/deduplication-handler.js +57 -13
- package/lib/handler/redirect-handler.js +8 -1
- package/lib/handler/retry-handler.js +71 -20
- package/lib/interceptor/decompress.js +238 -68
- package/lib/web/cookies/index.js +3 -1
- package/lib/web/cookies/parse.js +138 -137
- package/lib/web/cookies/util.js +21 -2
- package/lib/web/eventsource/eventsource-stream.js +90 -9
- package/lib/web/eventsource/eventsource.js +4 -1
- package/lib/web/fetch/index.js +3 -3
- package/lib/web/infra/index.js +14 -0
- package/lib/web/subresource-integrity/subresource-integrity.js +5 -5
- package/lib/web/websocket/connection.js +18 -8
- package/lib/web/websocket/receiver.js +13 -0
- package/package.json +1 -1
- package/types/client.d.ts +14 -0
- package/types/interceptors.d.ts +1 -1
package/docs/docs/api/Client.md
CHANGED
|
@@ -27,6 +27,8 @@ Returns: `Client`
|
|
|
27
27
|
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
28
28
|
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
|
|
29
29
|
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
|
30
|
+
* **eventSource** `EventSourceOptions` (optional) - EventSource-specific configuration options.
|
|
31
|
+
* **maxEventSize** `number` (optional) - The maximum total bytes of data and metadata field values retained for a pending EventSource event. Each unfinished line is separately limited to this value plus seven syntax bytes. The number of `data:` lines per event and input chunks buffered for an unfinished line are each limited to `min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead. Set to `0` to disable these limits. Default: `buffer.kStringMaxLength`.
|
|
30
32
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
|
31
33
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
32
34
|
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
|
@@ -53,7 +53,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>
|
|
|
53
53
|
|
|
54
54
|
## `undici:request:headers`
|
|
55
55
|
|
|
56
|
-
This message is published after the response headers have been received.
|
|
56
|
+
This message is published after the response headers have been received. This includes a successful CONNECT or
|
|
57
|
+
protocol upgrade response.
|
|
57
58
|
|
|
58
59
|
```js
|
|
59
60
|
import diagnosticsChannel from 'diagnostics_channel'
|
|
@@ -82,6 +83,7 @@ diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ requ
|
|
|
82
83
|
## `undici:request:trailers`
|
|
83
84
|
|
|
84
85
|
This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
|
86
|
+
After an upgraded socket is passed to the request handler, this message is published with an empty `trailers` array.
|
|
85
87
|
|
|
86
88
|
```js
|
|
87
89
|
import diagnosticsChannel from 'diagnostics_channel'
|
|
@@ -1153,6 +1153,7 @@ The `decompress` interceptor automatically decompresses response bodies that are
|
|
|
1153
1153
|
|
|
1154
1154
|
- `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`.
|
|
1155
1155
|
- `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`.
|
|
1156
|
+
- `maxSize` - Maximum decompressed response size in bytes for each decompression stage. Set to `0` to disable the limit. Default: `0`.
|
|
1156
1157
|
|
|
1157
1158
|
**Example - Basic Decompress Interceptor**
|
|
1158
1159
|
|
package/lib/core/connect.js
CHANGED
|
@@ -41,16 +41,22 @@ const SessionCache = class WeakSessionCache {
|
|
|
41
41
|
if (this._sessionCache.has(sessionKey)) {
|
|
42
42
|
this._sessionCache.delete(sessionKey)
|
|
43
43
|
} else if (this._sessionCache.size >= this._maxCachedSessions) {
|
|
44
|
+
// Make room for the new session: prefer an entry whose session has
|
|
45
|
+
// already been collected, otherwise evict the oldest one.
|
|
46
|
+
let evicted = false
|
|
44
47
|
for (const [key, ref] of this._sessionCache) {
|
|
45
48
|
if (ref.deref() === undefined) {
|
|
46
49
|
this._sessionCache.delete(key)
|
|
47
|
-
|
|
50
|
+
evicted = true
|
|
51
|
+
break
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if (!evicted) {
|
|
56
|
+
const oldest = this._sessionCache.keys().next()
|
|
57
|
+
if (!oldest.done) {
|
|
58
|
+
this._sessionCache.delete(oldest.value)
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
package/lib/core/request.js
CHANGED
|
@@ -302,11 +302,32 @@ class Request {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
/**
|
|
306
|
+
* @param {number} statusCode
|
|
307
|
+
* @param {Buffer[]|string[]} headers
|
|
308
|
+
* @param {import('node:stream').Duplex} socket
|
|
309
|
+
* @param {string} [statusText]
|
|
310
|
+
*/
|
|
311
|
+
onUpgrade (statusCode, headers, socket, statusText = '') {
|
|
312
|
+
this.onFinally()
|
|
313
|
+
|
|
306
314
|
assert(!this.aborted)
|
|
307
315
|
assert(!this.completed)
|
|
308
316
|
|
|
309
|
-
|
|
317
|
+
if (channels.headers.hasSubscribers) {
|
|
318
|
+
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = this[kHandler].onUpgrade(statusCode, headers, socket)
|
|
322
|
+
|
|
323
|
+
if (!this.aborted) {
|
|
324
|
+
this.completed = true
|
|
325
|
+
if (channels.trailers.hasSubscribers) {
|
|
326
|
+
channels.trailers.publish({ request: this, trailers: [] })
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return result
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
onComplete (trailers) {
|
package/lib/core/symbols.js
CHANGED
package/lib/dispatcher/agent.js
CHANGED
|
@@ -22,7 +22,7 @@ function defaultFactory (origin, opts) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
class Agent extends DispatcherBase {
|
|
25
|
-
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
|
|
25
|
+
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) {
|
|
26
26
|
if (typeof factory !== 'function') {
|
|
27
27
|
throw new InvalidArgumentError('factory must be a function.')
|
|
28
28
|
}
|
|
@@ -41,7 +41,11 @@ class Agent extends DispatcherBase {
|
|
|
41
41
|
connect = { ...connect }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if (tls && typeof tls !== 'function') {
|
|
45
|
+
tls = { ...tls }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls }
|
|
45
49
|
this[kFactory] = factory
|
|
46
50
|
this[kClients] = new Map()
|
|
47
51
|
this[kOrigins] = new Set()
|
|
@@ -69,8 +69,14 @@ function lazyllhttp () {
|
|
|
69
69
|
|
|
70
70
|
let mod
|
|
71
71
|
|
|
72
|
-
// We disable wasm SIMD on
|
|
73
|
-
let useWasmSIMD =
|
|
72
|
+
// We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures.
|
|
73
|
+
let useWasmSIMD = true
|
|
74
|
+
if (process.arch === 'ppc64') {
|
|
75
|
+
const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10))
|
|
76
|
+
if (major < 24 || (major === 24 && minor < 12)) {
|
|
77
|
+
useWasmSIMD = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
74
80
|
// The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
|
|
75
81
|
if (process.env.UNDICI_NO_WASM_SIMD === '1') {
|
|
76
82
|
useWasmSIMD = false
|
|
@@ -521,7 +527,7 @@ class Parser {
|
|
|
521
527
|
* @param {Buffer} head
|
|
522
528
|
*/
|
|
523
529
|
onUpgrade (head) {
|
|
524
|
-
const { upgrade, client, socket, headers, statusCode } = this
|
|
530
|
+
const { upgrade, client, socket, headers, statusCode, statusText } = this
|
|
525
531
|
|
|
526
532
|
assert(upgrade)
|
|
527
533
|
assert(client[kSocket] === socket)
|
|
@@ -556,8 +562,9 @@ class Parser {
|
|
|
556
562
|
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
|
557
563
|
|
|
558
564
|
try {
|
|
559
|
-
request.onUpgrade(statusCode, headers, socket)
|
|
565
|
+
request.onUpgrade(statusCode, headers, socket, statusText)
|
|
560
566
|
} catch (err) {
|
|
567
|
+
util.errorRequest(client, request, err)
|
|
561
568
|
util.destroy(socket, err)
|
|
562
569
|
}
|
|
563
570
|
|
|
@@ -921,8 +928,6 @@ function connectH1 (client, socket) {
|
|
|
921
928
|
}
|
|
922
929
|
|
|
923
930
|
function onHttpSocketError (err) {
|
|
924
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
925
|
-
|
|
926
931
|
const parser = this[kParser]
|
|
927
932
|
|
|
928
933
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
@@ -293,8 +293,6 @@ function onHttp2SendPing (session) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
function onHttp2SessionError (err) {
|
|
296
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
297
|
-
|
|
298
296
|
this[kSocket][kError] = err
|
|
299
297
|
this[kClient][kOnError](err)
|
|
300
298
|
}
|
|
@@ -436,8 +434,6 @@ function onHttp2SocketClose () {
|
|
|
436
434
|
}
|
|
437
435
|
|
|
438
436
|
function onHttp2SocketError (err) {
|
|
439
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
440
|
-
|
|
441
437
|
this[kError] = err
|
|
442
438
|
|
|
443
439
|
this[kClient][kOnError](err)
|
|
@@ -575,13 +571,17 @@ function writeH2 (client, request) {
|
|
|
575
571
|
|
|
576
572
|
stream = session.request(headers, { endStream: false, signal })
|
|
577
573
|
stream[kHTTP2Stream] = true
|
|
574
|
+
++session[kOpenStreams]
|
|
578
575
|
|
|
579
576
|
stream.once('response', (headers, _flags) => {
|
|
580
577
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
581
578
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
579
|
+
try {
|
|
580
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
581
|
+
} catch (err) {
|
|
582
|
+
abort(err)
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
585
|
completeRequest(client, request)
|
|
586
586
|
})
|
|
587
587
|
|
|
@@ -610,11 +610,16 @@ function writeH2 (client, request) {
|
|
|
610
610
|
// We disabled endStream to allow the user to write to the stream
|
|
611
611
|
stream = session.request(headers, { endStream: false, signal })
|
|
612
612
|
stream[kHTTP2Stream] = true
|
|
613
|
+
++session[kOpenStreams]
|
|
613
614
|
stream.on('response', headers => {
|
|
614
615
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
615
616
|
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
try {
|
|
618
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
619
|
+
} catch (err) {
|
|
620
|
+
abort(err)
|
|
621
|
+
return
|
|
622
|
+
}
|
|
618
623
|
completeRequest(client, request)
|
|
619
624
|
})
|
|
620
625
|
stream.on('error', abort)
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -115,7 +115,8 @@ class Client extends DispatcherBase {
|
|
|
115
115
|
initialWindowSize,
|
|
116
116
|
connectionWindowSize,
|
|
117
117
|
pingInterval,
|
|
118
|
-
webSocket
|
|
118
|
+
webSocket,
|
|
119
|
+
eventSource
|
|
119
120
|
} = {}) {
|
|
120
121
|
if (keepAlive !== undefined) {
|
|
121
122
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -223,7 +224,7 @@ class Client extends DispatcherBase {
|
|
|
223
224
|
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
super({ webSocket })
|
|
227
|
+
super({ webSocket, eventSource })
|
|
227
228
|
|
|
228
229
|
if (typeof connect !== 'function') {
|
|
229
230
|
connect = buildConnector({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const buffer = require('node:buffer')
|
|
3
4
|
const Dispatcher = require('./dispatcher')
|
|
4
5
|
const UnwrapHandler = require('../handler/unwrap-handler')
|
|
5
6
|
const {
|
|
@@ -12,6 +13,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy
|
|
|
12
13
|
const kOnDestroyed = Symbol('onDestroyed')
|
|
13
14
|
const kOnClosed = Symbol('onClosed')
|
|
14
15
|
const kWebSocketOptions = Symbol('webSocketOptions')
|
|
16
|
+
const kEventSourceOptions = Symbol('eventSourceOptions')
|
|
15
17
|
|
|
16
18
|
class DispatcherBase extends Dispatcher {
|
|
17
19
|
/** @type {boolean} */
|
|
@@ -32,6 +34,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
32
34
|
constructor (opts) {
|
|
33
35
|
super()
|
|
34
36
|
this[kWebSocketOptions] = opts?.webSocket ?? {}
|
|
37
|
+
this[kEventSourceOptions] = opts?.eventSource ?? {}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -44,6 +47,15 @@ class DispatcherBase extends Dispatcher {
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @returns {import('../../types/client').Client.EventSourceOptions}
|
|
52
|
+
*/
|
|
53
|
+
get eventSourceOptions () {
|
|
54
|
+
return {
|
|
55
|
+
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
/** @returns {boolean} */
|
|
48
60
|
get destroyed () {
|
|
49
61
|
return this[kDestroyed]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
|
+
const { kRedirectCacheControl } = require('../core/symbols')
|
|
4
5
|
const {
|
|
5
6
|
parseCacheControlHeader,
|
|
6
7
|
hasInvalidCacheControlDirective,
|
|
@@ -13,6 +14,19 @@ const { parseHttpDate } = require('../util/date.js')
|
|
|
13
14
|
|
|
14
15
|
function noop () {}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @param {string | URL} a
|
|
19
|
+
* @param {string | URL} b
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function sameOrigin (a, b) {
|
|
23
|
+
try {
|
|
24
|
+
return new URL(String(a)).origin === new URL(String(b)).origin
|
|
25
|
+
} catch {
|
|
26
|
+
return String(a) === String(b)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
// Status codes that we can use some heuristics on to cache
|
|
17
31
|
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
|
|
18
32
|
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
|
|
@@ -151,6 +165,14 @@ class CacheHandler {
|
|
|
151
165
|
*/
|
|
152
166
|
#writeStream
|
|
153
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The URLs visited while following redirects, shared by reference from the
|
|
170
|
+
* redirect handler through the request context. Carries the suppressed hops'
|
|
171
|
+
* cache-control under {@link kRedirectCacheControl}.
|
|
172
|
+
* @type {URL[] | undefined}
|
|
173
|
+
*/
|
|
174
|
+
#history
|
|
175
|
+
|
|
154
176
|
/**
|
|
155
177
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
|
|
156
178
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
@@ -167,6 +189,9 @@ class CacheHandler {
|
|
|
167
189
|
onRequestStart (controller, context) {
|
|
168
190
|
this.#writeStream?.destroy()
|
|
169
191
|
this.#writeStream = undefined
|
|
192
|
+
// The redirect handler mutates this shared history as redirects are
|
|
193
|
+
// followed, allowing the cache to identify the response's actual origin.
|
|
194
|
+
this.#history = context?.history
|
|
170
195
|
this.#handler.onRequestStart?.(controller, context)
|
|
171
196
|
}
|
|
172
197
|
|
|
@@ -206,6 +231,29 @@ class CacheHandler {
|
|
|
206
231
|
return downstreamOnHeaders()
|
|
207
232
|
}
|
|
208
233
|
|
|
234
|
+
// Never store a response produced by a different origin under the original
|
|
235
|
+
// request's cache key.
|
|
236
|
+
if (this.#history && this.#history.length > 0) {
|
|
237
|
+
const finalUrl = this.#history[this.#history.length - 1]
|
|
238
|
+
if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
|
|
239
|
+
return downstreamOnHeaders()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Preserve restrictive cache policy from suppressed redirect responses.
|
|
244
|
+
const redirectCacheControl = this.#history?.[kRedirectCacheControl]
|
|
245
|
+
if (redirectCacheControl) {
|
|
246
|
+
for (const hopCacheControl of redirectCacheControl) {
|
|
247
|
+
const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
|
|
248
|
+
if (
|
|
249
|
+
hopDirectives['no-store'] === true ||
|
|
250
|
+
(this.#cacheType === 'shared' && hopDirectives.private === true)
|
|
251
|
+
) {
|
|
252
|
+
return downstreamOnHeaders()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
209
257
|
const cacheControlHeader = resHeaders['cache-control']
|
|
210
258
|
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
211
259
|
|
|
@@ -16,6 +16,7 @@ const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024
|
|
|
16
16
|
* @property {number} bufferedBytes
|
|
17
17
|
* @property {object | null} pendingTrailers
|
|
18
18
|
* @property {boolean} done
|
|
19
|
+
* @property {boolean} [pendingStart] onRequestStart deferred until the primary request's context is known
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -30,6 +31,20 @@ class DeduplicationHandler {
|
|
|
30
31
|
*/
|
|
31
32
|
#primaryHandler
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether the primary request has started (onRequestStart received). Until
|
|
36
|
+
* then the request context is unknown and cannot be given to waiting handlers.
|
|
37
|
+
* @type {boolean}
|
|
38
|
+
*/
|
|
39
|
+
#requestStarted = false
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The primary request context, replayed to waiting handlers so they observe
|
|
43
|
+
* the same redirect history as the primary.
|
|
44
|
+
* @type {any}
|
|
45
|
+
*/
|
|
46
|
+
#context = null
|
|
47
|
+
|
|
33
48
|
/**
|
|
34
49
|
* @type {WaitingHandler[]}
|
|
35
50
|
*/
|
|
@@ -112,20 +127,30 @@ class DeduplicationHandler {
|
|
|
112
127
|
const waitingController = waitingHandler.controller
|
|
113
128
|
|
|
114
129
|
try {
|
|
115
|
-
|
|
130
|
+
if (this.#requestStarted) {
|
|
131
|
+
// The primary request has started, so its context (including the
|
|
132
|
+
// redirect history) is known and can be delivered immediately.
|
|
133
|
+
handler.onRequestStart?.(waitingController, this.#context)
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
if (waitingController.aborted) {
|
|
136
|
+
waitingHandler.done = true
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
if (this.#responseStarted) {
|
|
141
|
+
handler.onResponseStart?.(
|
|
142
|
+
waitingController,
|
|
143
|
+
this.#statusCode,
|
|
144
|
+
this.#headers,
|
|
145
|
+
this.#statusMessage
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// The primary request has not started yet. Defer onRequestStart until
|
|
150
|
+
// its context is known, so the waiting handler observes the same
|
|
151
|
+
// redirect history as the primary (a cache handler relies on it to
|
|
152
|
+
// reject cross-origin redirect responses).
|
|
153
|
+
waitingHandler.pendingStart = true
|
|
129
154
|
}
|
|
130
155
|
} catch {
|
|
131
156
|
// Ignore errors from waiting handlers
|
|
@@ -146,6 +171,24 @@ class DeduplicationHandler {
|
|
|
146
171
|
*/
|
|
147
172
|
onRequestStart (controller, context) {
|
|
148
173
|
this.#controller = controller
|
|
174
|
+
if (!this.#requestStarted) {
|
|
175
|
+
this.#requestStarted = true
|
|
176
|
+
this.#context = context
|
|
177
|
+
// Deliver the now-known context to handlers that joined before the
|
|
178
|
+
// primary request started.
|
|
179
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
180
|
+
if (waitingHandler.pendingStart && !waitingHandler.done && !waitingHandler.controller.aborted) {
|
|
181
|
+
waitingHandler.pendingStart = false
|
|
182
|
+
try {
|
|
183
|
+
waitingHandler.handler.onRequestStart?.(waitingHandler.controller, context)
|
|
184
|
+
} catch {
|
|
185
|
+
// Ignore errors from waiting handlers
|
|
186
|
+
waitingHandler.done = true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.#pruneDoneWaitingHandlers()
|
|
191
|
+
}
|
|
149
192
|
this.#primaryHandler.onRequestStart?.(controller, context)
|
|
150
193
|
}
|
|
151
194
|
|
|
@@ -319,7 +362,8 @@ class DeduplicationHandler {
|
|
|
319
362
|
bufferedChunks: [],
|
|
320
363
|
bufferedBytes: 0,
|
|
321
364
|
pendingTrailers: null,
|
|
322
|
-
done: false
|
|
365
|
+
done: false,
|
|
366
|
+
pendingStart: false
|
|
323
367
|
}
|
|
324
368
|
|
|
325
369
|
const state = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
|
-
const { kBodyUsed } = require('../core/symbols')
|
|
4
|
+
const { kBodyUsed, kRedirectCacheControl } = require('../core/symbols')
|
|
5
5
|
const assert = require('node:assert')
|
|
6
6
|
const { InvalidArgumentError } = require('../core/errors')
|
|
7
7
|
const EE = require('node:events')
|
|
@@ -47,6 +47,11 @@ class RedirectHandler {
|
|
|
47
47
|
this.maxRedirections = maxRedirections
|
|
48
48
|
this.handler = handler
|
|
49
49
|
this.history = []
|
|
50
|
+
// Retain cache-control from suppressed redirect hops for an outer cache.
|
|
51
|
+
Object.defineProperty(this.history, kRedirectCacheControl, {
|
|
52
|
+
value: [],
|
|
53
|
+
enumerable: false
|
|
54
|
+
})
|
|
50
55
|
|
|
51
56
|
if (util.isStream(this.opts.body)) {
|
|
52
57
|
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
|
|
@@ -130,6 +135,8 @@ class RedirectHandler {
|
|
|
130
135
|
return
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
this.history[kRedirectCacheControl].push(headers['cache-control'])
|
|
139
|
+
|
|
133
140
|
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
|
|
134
141
|
const path = search ? `${pathname}${search}` : pathname
|
|
135
142
|
|