undici 7.29.0 → 7.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/DiagnosticsChannel.md +3 -1
- package/docs/docs/api/Dispatcher.md +1 -0
- package/lib/core/request.js +23 -2
- package/lib/dispatcher/balanced-pool.js +4 -2
- package/lib/dispatcher/client-h1.js +23 -8
- package/lib/dispatcher/client-h2.js +84 -19
- package/lib/dispatcher/client.js +6 -2
- package/lib/handler/cache-handler.js +21 -5
- package/lib/handler/retry-handler.js +109 -23
- package/lib/interceptor/cache.js +20 -1
- package/lib/interceptor/decompress.js +338 -46
- package/lib/interceptor/dump.js +10 -23
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/websocket/connection.js +19 -9
- 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
|
@@ -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/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) {
|
|
@@ -49,14 +49,16 @@ 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
|
-
|
|
59
|
+
if (connect && typeof connect !== 'function') connect = { ...connect }
|
|
60
|
+
if (tls && typeof tls !== 'function') tls = { ...tls }
|
|
61
|
+
this[kOptions] = { ...util.deepClone(opts), connect, tls }
|
|
60
62
|
this[kOptions].interceptors = opts.interceptors
|
|
61
63
|
? { ...opts.interceptors }
|
|
62
64
|
: undefined
|
|
@@ -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
|
|
|
@@ -1012,7 +1019,7 @@ function onSocketClose () {
|
|
|
1012
1019
|
|
|
1013
1020
|
function clearIdleSocketValidation (socket) {
|
|
1014
1021
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
1015
|
-
|
|
1022
|
+
clearImmediate(socket[kIdleSocketValidationTimeout])
|
|
1016
1023
|
socket[kIdleSocketValidationTimeout] = null
|
|
1017
1024
|
}
|
|
1018
1025
|
|
|
@@ -1021,15 +1028,23 @@ function clearIdleSocketValidation (socket) {
|
|
|
1021
1028
|
|
|
1022
1029
|
function scheduleIdleSocketValidation (client, socket) {
|
|
1023
1030
|
socket[kIdleSocketValidation] = 1
|
|
1024
|
-
|
|
1031
|
+
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
|
1032
|
+
// already pending on this idle keep-alive socket are processed before the
|
|
1033
|
+
// next request is written (GHSA-35p6-xmwp-9g52).
|
|
1034
|
+
//
|
|
1035
|
+
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
|
1036
|
+
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
|
1037
|
+
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
|
1038
|
+
// A ref'd Immediate both keeps the pending request alive and makes poll
|
|
1039
|
+
// return immediately — the hybrid those issues asked for.
|
|
1040
|
+
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
|
1025
1041
|
socket[kIdleSocketValidationTimeout] = null
|
|
1026
1042
|
socket[kIdleSocketValidation] = 2
|
|
1027
1043
|
|
|
1028
1044
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
1029
1045
|
client[kResume]()
|
|
1030
1046
|
}
|
|
1031
|
-
}
|
|
1032
|
-
socket[kIdleSocketValidationTimeout].unref?.()
|
|
1047
|
+
})
|
|
1033
1048
|
}
|
|
1034
1049
|
|
|
1035
1050
|
/**
|
|
@@ -8,7 +8,9 @@ const {
|
|
|
8
8
|
RequestAbortedError,
|
|
9
9
|
SocketError,
|
|
10
10
|
InformationalError,
|
|
11
|
-
InvalidArgumentError
|
|
11
|
+
InvalidArgumentError,
|
|
12
|
+
HeadersTimeoutError,
|
|
13
|
+
BodyTimeoutError
|
|
12
14
|
} = require('../core/errors.js')
|
|
13
15
|
const {
|
|
14
16
|
kUrl,
|
|
@@ -33,6 +35,7 @@ const {
|
|
|
33
35
|
kHTTPContext,
|
|
34
36
|
kClosed,
|
|
35
37
|
kBodyTimeout,
|
|
38
|
+
kHeadersTimeout,
|
|
36
39
|
kEnableConnectProtocol,
|
|
37
40
|
kRemoteSettings,
|
|
38
41
|
kHTTP2Stream,
|
|
@@ -219,7 +222,11 @@ function resumeH2 (client) {
|
|
|
219
222
|
const socket = client[kSocket]
|
|
220
223
|
|
|
221
224
|
if (socket?.destroyed === false) {
|
|
222
|
-
|
|
225
|
+
// Only let the process exit when there is genuinely nothing outstanding.
|
|
226
|
+
// Unreffing because the peer advertised MAX_CONCURRENT_STREAMS = 0 left
|
|
227
|
+
// queued requests with nothing holding the event loop open, so the process
|
|
228
|
+
// could exit with status 0 while an awaited request never settled.
|
|
229
|
+
if (client[kSize] === 0) {
|
|
223
230
|
socket.unref()
|
|
224
231
|
client[kHTTP2Session].unref()
|
|
225
232
|
} else {
|
|
@@ -314,6 +321,36 @@ function onHttp2SessionEnd () {
|
|
|
314
321
|
* @this {import('http2').ClientHttp2Session}
|
|
315
322
|
* @param {number} errorCode
|
|
316
323
|
*/
|
|
324
|
+
// Backport of #5410 and #5569. HTTP/2 multiplexes, so requests complete out of
|
|
325
|
+
// order; advancing kRunningIdx blindly retired whichever request happened to
|
|
326
|
+
// sit at the head instead of the one that actually finished, which both lost
|
|
327
|
+
// requests and left phantom running slots behind.
|
|
328
|
+
function completeRequest (client, request, resetPendingIdx = false) {
|
|
329
|
+
const queue = client[kQueue]
|
|
330
|
+
const runningIdx = client[kRunningIdx]
|
|
331
|
+
|
|
332
|
+
// In-order completion: clear the request and advance without splicing.
|
|
333
|
+
// The client's resume loop compacts cleared slots once the index grows.
|
|
334
|
+
if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
|
|
335
|
+
queue[runningIdx] = null
|
|
336
|
+
client[kRunningIdx] = runningIdx + 1
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const index = queue.indexOf(request, runningIdx)
|
|
341
|
+
|
|
342
|
+
if (index === -1 || index >= client[kPendingIdx]) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
queue.splice(index, 1)
|
|
347
|
+
client[kPendingIdx]--
|
|
348
|
+
|
|
349
|
+
if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
|
|
350
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
317
354
|
function onHttp2SessionGoAway (errorCode) {
|
|
318
355
|
// TODO(mcollina): Verify if GOAWAY implements the spec correctly:
|
|
319
356
|
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
|
|
@@ -335,7 +372,9 @@ function onHttp2SessionGoAway (errorCode) {
|
|
|
335
372
|
if (client[kRunningIdx] < client[kQueue].length) {
|
|
336
373
|
const request = client[kQueue][client[kRunningIdx]]
|
|
337
374
|
client[kQueue][client[kRunningIdx]++] = null
|
|
338
|
-
|
|
375
|
+
if (request != null) {
|
|
376
|
+
util.errorRequest(client, request, err)
|
|
377
|
+
}
|
|
339
378
|
client[kPendingIdx] = client[kRunningIdx]
|
|
340
379
|
}
|
|
341
380
|
|
|
@@ -368,7 +407,9 @@ function onHttp2SessionClose () {
|
|
|
368
407
|
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
369
408
|
for (let i = 0; i < requests.length; i++) {
|
|
370
409
|
const request = requests[i]
|
|
371
|
-
|
|
410
|
+
if (request != null) {
|
|
411
|
+
util.errorRequest(client, request, err)
|
|
412
|
+
}
|
|
372
413
|
}
|
|
373
414
|
}
|
|
374
415
|
}
|
|
@@ -416,7 +457,10 @@ function shouldSendContentLength (method) {
|
|
|
416
457
|
}
|
|
417
458
|
|
|
418
459
|
function writeH2 (client, request) {
|
|
419
|
-
|
|
460
|
+
// Time to the response headers, then time between body chunks. Using
|
|
461
|
+
// bodyTimeout for both made headersTimeout a no-op over HTTP/2.
|
|
462
|
+
const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
|
|
463
|
+
const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
|
|
420
464
|
const session = client[kHTTP2Session]
|
|
421
465
|
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
|
|
422
466
|
let { body } = request
|
|
@@ -483,6 +527,7 @@ function writeH2 (client, request) {
|
|
|
483
527
|
|
|
484
528
|
// We move the running index to the next request
|
|
485
529
|
client[kOnError](err)
|
|
530
|
+
completeRequest(client, request)
|
|
486
531
|
client[kResume]()
|
|
487
532
|
}
|
|
488
533
|
|
|
@@ -530,14 +575,18 @@ function writeH2 (client, request) {
|
|
|
530
575
|
|
|
531
576
|
stream = session.request(headers, { endStream: false, signal })
|
|
532
577
|
stream[kHTTP2Stream] = true
|
|
578
|
+
++session[kOpenStreams]
|
|
533
579
|
|
|
534
580
|
stream.once('response', (headers, _flags) => {
|
|
535
581
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
536
582
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
583
|
+
try {
|
|
584
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
585
|
+
} catch (err) {
|
|
586
|
+
abort(err)
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
completeRequest(client, request)
|
|
541
590
|
})
|
|
542
591
|
|
|
543
592
|
stream.on('error', () => {
|
|
@@ -554,7 +603,7 @@ function writeH2 (client, request) {
|
|
|
554
603
|
if (session[kOpenStreams] === 0) session.unref()
|
|
555
604
|
})
|
|
556
605
|
|
|
557
|
-
stream.setTimeout(
|
|
606
|
+
stream.setTimeout(headersTimeout)
|
|
558
607
|
return true
|
|
559
608
|
}
|
|
560
609
|
|
|
@@ -565,18 +614,24 @@ function writeH2 (client, request) {
|
|
|
565
614
|
// We disabled endStream to allow the user to write to the stream
|
|
566
615
|
stream = session.request(headers, { endStream: false, signal })
|
|
567
616
|
stream[kHTTP2Stream] = true
|
|
617
|
+
++session[kOpenStreams]
|
|
568
618
|
stream.on('response', headers => {
|
|
569
619
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
570
620
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
621
|
+
try {
|
|
622
|
+
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
623
|
+
} catch (err) {
|
|
624
|
+
abort(err)
|
|
625
|
+
return
|
|
626
|
+
}
|
|
627
|
+
completeRequest(client, request)
|
|
574
628
|
})
|
|
629
|
+
stream.on('error', abort)
|
|
575
630
|
stream.once('close', () => {
|
|
576
631
|
session[kOpenStreams] -= 1
|
|
577
632
|
if (session[kOpenStreams] === 0) session.unref()
|
|
578
633
|
})
|
|
579
|
-
stream.setTimeout(
|
|
634
|
+
stream.setTimeout(headersTimeout)
|
|
580
635
|
|
|
581
636
|
return true
|
|
582
637
|
}
|
|
@@ -677,7 +732,7 @@ function writeH2 (client, request) {
|
|
|
677
732
|
|
|
678
733
|
// Increment counter as we have new streams open
|
|
679
734
|
++session[kOpenStreams]
|
|
680
|
-
stream.setTimeout(
|
|
735
|
+
stream.setTimeout(headersTimeout)
|
|
681
736
|
|
|
682
737
|
// Track whether we received a response (headers)
|
|
683
738
|
let responseReceived = false
|
|
@@ -686,6 +741,7 @@ function writeH2 (client, request) {
|
|
|
686
741
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
687
742
|
request.onResponseStarted()
|
|
688
743
|
responseReceived = true
|
|
744
|
+
stream.setTimeout(bodyTimeout)
|
|
689
745
|
|
|
690
746
|
// Due to the stream nature, it is possible we face a race condition
|
|
691
747
|
// where the stream has been assigned, but the request has been aborted
|
|
@@ -720,14 +776,13 @@ function writeH2 (client, request) {
|
|
|
720
776
|
request.onComplete({})
|
|
721
777
|
}
|
|
722
778
|
|
|
723
|
-
client
|
|
779
|
+
completeRequest(client, request)
|
|
724
780
|
client[kResume]()
|
|
725
781
|
} else {
|
|
726
782
|
// Stream ended without receiving a response - this is an error
|
|
727
783
|
// (e.g., server destroyed the stream before sending headers)
|
|
728
784
|
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
|
|
729
|
-
client
|
|
730
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
785
|
+
completeRequest(client, request, true)
|
|
731
786
|
client[kResume]()
|
|
732
787
|
}
|
|
733
788
|
})
|
|
@@ -738,6 +793,14 @@ function writeH2 (client, request) {
|
|
|
738
793
|
if (session[kOpenStreams] === 0) {
|
|
739
794
|
session.unref()
|
|
740
795
|
}
|
|
796
|
+
|
|
797
|
+
// A stream can close without ever emitting 'end' or 'error': a peer's
|
|
798
|
+
// RST_STREAM(CANCEL) received before the response is reported by Node as a
|
|
799
|
+
// bare 'close', and destroying the stream unenrolls its timeout, so no
|
|
800
|
+
// 'timeout' follows either. Nothing else would ever settle this request.
|
|
801
|
+
if (!request.aborted && !request.completed) {
|
|
802
|
+
abort(new InformationalError('HTTP/2: stream closed before the response was complete'))
|
|
803
|
+
}
|
|
741
804
|
})
|
|
742
805
|
|
|
743
806
|
stream.once('error', function (err) {
|
|
@@ -755,7 +818,9 @@ function writeH2 (client, request) {
|
|
|
755
818
|
})
|
|
756
819
|
|
|
757
820
|
stream.on('timeout', () => {
|
|
758
|
-
const err =
|
|
821
|
+
const err = responseReceived
|
|
822
|
+
? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
|
|
823
|
+
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
|
|
759
824
|
stream.removeAllListeners('data')
|
|
760
825
|
session[kOpenStreams] -= 1
|
|
761
826
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -374,7 +374,9 @@ class Client extends DispatcherBase {
|
|
|
374
374
|
const requests = this[kQueue].splice(this[kPendingIdx])
|
|
375
375
|
for (let i = 0; i < requests.length; i++) {
|
|
376
376
|
const request = requests[i]
|
|
377
|
-
|
|
377
|
+
if (request != null) {
|
|
378
|
+
util.errorRequest(this, request, err)
|
|
379
|
+
}
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
const callback = () => {
|
|
@@ -413,7 +415,9 @@ function onError (client, err) {
|
|
|
413
415
|
|
|
414
416
|
for (let i = 0; i < requests.length; i++) {
|
|
415
417
|
const request = requests[i]
|
|
416
|
-
|
|
418
|
+
if (request != null) {
|
|
419
|
+
util.errorRequest(client, request, err)
|
|
420
|
+
}
|
|
417
421
|
}
|
|
418
422
|
assert(client[kSize] === 0)
|
|
419
423
|
}
|
|
@@ -207,6 +207,13 @@ class CacheHandler {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
const cacheControlHeader = resHeaders['cache-control']
|
|
210
|
+
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
211
|
+
|
|
212
|
+
if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) {
|
|
213
|
+
deleteCachedValue(this.#store, this.#cacheKey)
|
|
214
|
+
return downstreamOnHeaders()
|
|
215
|
+
}
|
|
216
|
+
|
|
210
217
|
const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
|
|
211
218
|
if (
|
|
212
219
|
!cacheControlHeader &&
|
|
@@ -223,8 +230,7 @@ class CacheHandler {
|
|
|
223
230
|
return downstreamOnHeaders()
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
|
|
227
|
-
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
|
|
233
|
+
if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
|
|
228
234
|
if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
|
|
229
235
|
deleteCachedValue(this.#store, this.#cacheKey)
|
|
230
236
|
}
|
|
@@ -465,7 +471,10 @@ function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
|
|
|
465
471
|
*/
|
|
466
472
|
function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
|
|
467
473
|
return cacheControlDirectives['no-store'] === true ||
|
|
468
|
-
(cacheType === 'shared' &&
|
|
474
|
+
(cacheType === 'shared' && (
|
|
475
|
+
cacheControlDirectives.private === true ||
|
|
476
|
+
Object.hasOwn(resHeaders, 'set-cookie')
|
|
477
|
+
)) ||
|
|
469
478
|
(resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
|
|
470
479
|
}
|
|
471
480
|
|
|
@@ -473,12 +482,16 @@ function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheC
|
|
|
473
482
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
474
483
|
*
|
|
475
484
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
485
|
+
* @param {string} method
|
|
476
486
|
* @param {number} statusCode
|
|
477
487
|
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
478
488
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
479
489
|
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
|
|
480
490
|
*/
|
|
481
|
-
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
|
|
491
|
+
function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
|
|
492
|
+
if (!arrayIncludes(util.safeHTTPMethods, method)) {
|
|
493
|
+
return false
|
|
494
|
+
}
|
|
482
495
|
// Status code must be final and understood.
|
|
483
496
|
if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
|
|
484
497
|
return false
|
|
@@ -499,7 +512,10 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
|
|
|
499
512
|
return false
|
|
500
513
|
}
|
|
501
514
|
|
|
502
|
-
if (cacheType === 'shared' &&
|
|
515
|
+
if (cacheType === 'shared' && (
|
|
516
|
+
cacheControlDirectives.private === true ||
|
|
517
|
+
Object.hasOwn(resHeaders, 'set-cookie')
|
|
518
|
+
)) {
|
|
503
519
|
return false
|
|
504
520
|
}
|
|
505
521
|
|