undici 7.29.0 → 7.29.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/lib/dispatcher/balanced-pool.js +4 -2
- package/lib/dispatcher/client-h1.js +12 -4
- package/lib/dispatcher/client-h2.js +70 -14
- package/lib/dispatcher/client.js +6 -2
- package/lib/handler/cache-handler.js +21 -5
- package/lib/handler/retry-handler.js +42 -7
- package/lib/interceptor/cache.js +20 -1
- package/lib/interceptor/decompress.js +146 -14
- package/lib/interceptor/dump.js +10 -23
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- 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
|
@@ -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
|
|
@@ -1012,7 +1012,7 @@ function onSocketClose () {
|
|
|
1012
1012
|
|
|
1013
1013
|
function clearIdleSocketValidation (socket) {
|
|
1014
1014
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
1015
|
-
|
|
1015
|
+
clearImmediate(socket[kIdleSocketValidationTimeout])
|
|
1016
1016
|
socket[kIdleSocketValidationTimeout] = null
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
@@ -1021,15 +1021,23 @@ function clearIdleSocketValidation (socket) {
|
|
|
1021
1021
|
|
|
1022
1022
|
function scheduleIdleSocketValidation (client, socket) {
|
|
1023
1023
|
socket[kIdleSocketValidation] = 1
|
|
1024
|
-
|
|
1024
|
+
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
|
1025
|
+
// already pending on this idle keep-alive socket are processed before the
|
|
1026
|
+
// next request is written (GHSA-35p6-xmwp-9g52).
|
|
1027
|
+
//
|
|
1028
|
+
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
|
1029
|
+
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
|
1030
|
+
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
|
1031
|
+
// A ref'd Immediate both keeps the pending request alive and makes poll
|
|
1032
|
+
// return immediately — the hybrid those issues asked for.
|
|
1033
|
+
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
|
1025
1034
|
socket[kIdleSocketValidationTimeout] = null
|
|
1026
1035
|
socket[kIdleSocketValidation] = 2
|
|
1027
1036
|
|
|
1028
1037
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
1029
1038
|
client[kResume]()
|
|
1030
1039
|
}
|
|
1031
|
-
}
|
|
1032
|
-
socket[kIdleSocketValidationTimeout].unref?.()
|
|
1040
|
+
})
|
|
1033
1041
|
}
|
|
1034
1042
|
|
|
1035
1043
|
/**
|
|
@@ -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
|
|
|
@@ -537,7 +582,7 @@ function writeH2 (client, request) {
|
|
|
537
582
|
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
538
583
|
|
|
539
584
|
++session[kOpenStreams]
|
|
540
|
-
client
|
|
585
|
+
completeRequest(client, request)
|
|
541
586
|
})
|
|
542
587
|
|
|
543
588
|
stream.on('error', () => {
|
|
@@ -554,7 +599,7 @@ function writeH2 (client, request) {
|
|
|
554
599
|
if (session[kOpenStreams] === 0) session.unref()
|
|
555
600
|
})
|
|
556
601
|
|
|
557
|
-
stream.setTimeout(
|
|
602
|
+
stream.setTimeout(headersTimeout)
|
|
558
603
|
return true
|
|
559
604
|
}
|
|
560
605
|
|
|
@@ -570,13 +615,14 @@ function writeH2 (client, request) {
|
|
|
570
615
|
|
|
571
616
|
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
572
617
|
++session[kOpenStreams]
|
|
573
|
-
client
|
|
618
|
+
completeRequest(client, request)
|
|
574
619
|
})
|
|
620
|
+
stream.on('error', abort)
|
|
575
621
|
stream.once('close', () => {
|
|
576
622
|
session[kOpenStreams] -= 1
|
|
577
623
|
if (session[kOpenStreams] === 0) session.unref()
|
|
578
624
|
})
|
|
579
|
-
stream.setTimeout(
|
|
625
|
+
stream.setTimeout(headersTimeout)
|
|
580
626
|
|
|
581
627
|
return true
|
|
582
628
|
}
|
|
@@ -677,7 +723,7 @@ function writeH2 (client, request) {
|
|
|
677
723
|
|
|
678
724
|
// Increment counter as we have new streams open
|
|
679
725
|
++session[kOpenStreams]
|
|
680
|
-
stream.setTimeout(
|
|
726
|
+
stream.setTimeout(headersTimeout)
|
|
681
727
|
|
|
682
728
|
// Track whether we received a response (headers)
|
|
683
729
|
let responseReceived = false
|
|
@@ -686,6 +732,7 @@ function writeH2 (client, request) {
|
|
|
686
732
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
687
733
|
request.onResponseStarted()
|
|
688
734
|
responseReceived = true
|
|
735
|
+
stream.setTimeout(bodyTimeout)
|
|
689
736
|
|
|
690
737
|
// Due to the stream nature, it is possible we face a race condition
|
|
691
738
|
// where the stream has been assigned, but the request has been aborted
|
|
@@ -720,14 +767,13 @@ function writeH2 (client, request) {
|
|
|
720
767
|
request.onComplete({})
|
|
721
768
|
}
|
|
722
769
|
|
|
723
|
-
client
|
|
770
|
+
completeRequest(client, request)
|
|
724
771
|
client[kResume]()
|
|
725
772
|
} else {
|
|
726
773
|
// Stream ended without receiving a response - this is an error
|
|
727
774
|
// (e.g., server destroyed the stream before sending headers)
|
|
728
775
|
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
|
|
729
|
-
client
|
|
730
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
776
|
+
completeRequest(client, request, true)
|
|
731
777
|
client[kResume]()
|
|
732
778
|
}
|
|
733
779
|
})
|
|
@@ -738,6 +784,14 @@ function writeH2 (client, request) {
|
|
|
738
784
|
if (session[kOpenStreams] === 0) {
|
|
739
785
|
session.unref()
|
|
740
786
|
}
|
|
787
|
+
|
|
788
|
+
// A stream can close without ever emitting 'end' or 'error': a peer's
|
|
789
|
+
// RST_STREAM(CANCEL) received before the response is reported by Node as a
|
|
790
|
+
// bare 'close', and destroying the stream unenrolls its timeout, so no
|
|
791
|
+
// 'timeout' follows either. Nothing else would ever settle this request.
|
|
792
|
+
if (!request.aborted && !request.completed) {
|
|
793
|
+
abort(new InformationalError('HTTP/2: stream closed before the response was complete'))
|
|
794
|
+
}
|
|
741
795
|
})
|
|
742
796
|
|
|
743
797
|
stream.once('error', function (err) {
|
|
@@ -755,7 +809,9 @@ function writeH2 (client, request) {
|
|
|
755
809
|
})
|
|
756
810
|
|
|
757
811
|
stream.on('timeout', () => {
|
|
758
|
-
const err =
|
|
812
|
+
const err = responseReceived
|
|
813
|
+
? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
|
|
814
|
+
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
|
|
759
815
|
stream.removeAllListeners('data')
|
|
760
816
|
session[kOpenStreams] -= 1
|
|
761
817
|
|
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
|
|
|
@@ -95,8 +95,16 @@ class RetryHandler {
|
|
|
95
95
|
if (this.retryOpts.throwOnError) {
|
|
96
96
|
// Preserve old behavior for status codes that are not eligible for retry
|
|
97
97
|
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
|
98
|
-
this.headersSent
|
|
99
|
-
|
|
98
|
+
if (this.headersSent) {
|
|
99
|
+
// The downstream handler already received the response from an
|
|
100
|
+
// earlier attempt. Forwarding this response would replace the
|
|
101
|
+
// downstream body and leave the original body pending forever.
|
|
102
|
+
this.handler.onResponseError?.(controller, err)
|
|
103
|
+
} else {
|
|
104
|
+
this.headersSent = true
|
|
105
|
+
this.checkpointResponseEnd(headers)
|
|
106
|
+
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
|
107
|
+
}
|
|
100
108
|
} else {
|
|
101
109
|
this.error = err
|
|
102
110
|
}
|
|
@@ -106,14 +114,23 @@ class RetryHandler {
|
|
|
106
114
|
|
|
107
115
|
if (isDisturbed(this.opts.body)) {
|
|
108
116
|
this.headersSent = true
|
|
117
|
+
this.checkpointResponseEnd(headers)
|
|
109
118
|
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
|
110
119
|
return
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
function shouldRetry (passedErr) {
|
|
114
123
|
if (passedErr) {
|
|
115
|
-
this.headersSent
|
|
116
|
-
|
|
124
|
+
if (this.headersSent) {
|
|
125
|
+
// The downstream handler already received the response from an
|
|
126
|
+
// earlier attempt. Forwarding this response would replace the
|
|
127
|
+
// downstream body and leave the original body pending forever.
|
|
128
|
+
this.handler.onResponseError?.(controller, passedErr)
|
|
129
|
+
} else {
|
|
130
|
+
this.headersSent = true
|
|
131
|
+
this.checkpointResponseEnd(headers)
|
|
132
|
+
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
|
133
|
+
}
|
|
117
134
|
controller.resume()
|
|
118
135
|
return
|
|
119
136
|
}
|
|
@@ -133,6 +150,20 @@ class RetryHandler {
|
|
|
133
150
|
)
|
|
134
151
|
}
|
|
135
152
|
|
|
153
|
+
checkpointResponseEnd (headers) {
|
|
154
|
+
if (this.end == null && this.opts.method !== 'HEAD') {
|
|
155
|
+
const contentLength = headers['content-length']
|
|
156
|
+
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
157
|
+
|
|
158
|
+
assert(
|
|
159
|
+
this.end == null || Number.isFinite(this.end),
|
|
160
|
+
'invalid content-length'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
this.resume = this.end != null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
136
167
|
onRequestStart (controller, context) {
|
|
137
168
|
if (!this.headersSent) {
|
|
138
169
|
this.handler.onRequestStart?.(controller, context)
|
|
@@ -253,8 +284,12 @@ class RetryHandler {
|
|
|
253
284
|
|
|
254
285
|
const { start, size, end = size ? size - 1 : null } = contentRange
|
|
255
286
|
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
if (this.start !== start || (this.end != null && this.end !== end)) {
|
|
288
|
+
throw new RequestRetryError('Content-Range mismatch', statusCode, {
|
|
289
|
+
headers,
|
|
290
|
+
data: { count: this.retryCount }
|
|
291
|
+
})
|
|
292
|
+
}
|
|
258
293
|
|
|
259
294
|
return
|
|
260
295
|
}
|
|
@@ -379,7 +414,7 @@ class RetryHandler {
|
|
|
379
414
|
}
|
|
380
415
|
|
|
381
416
|
onResponseError (controller, err) {
|
|
382
|
-
if (controller?.aborted || isDisturbed(this.opts.body)) {
|
|
417
|
+
if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) {
|
|
383
418
|
this.handler.onResponseError?.(controller, err)
|
|
384
419
|
return
|
|
385
420
|
}
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -117,7 +117,10 @@ function staleResponseRequiresRevalidation (result, cacheType) {
|
|
|
117
117
|
* @returns {boolean}
|
|
118
118
|
*/
|
|
119
119
|
function revalidationResponseDisallowsCachedReuse (cacheType, headers) {
|
|
120
|
-
if (
|
|
120
|
+
if (
|
|
121
|
+
(headers.vary && isInvalidOrWildcardVaryHeader(headers.vary)) ||
|
|
122
|
+
(cacheType === 'shared' && Object.hasOwn(headers, 'set-cookie'))
|
|
123
|
+
) {
|
|
121
124
|
return true
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -376,6 +379,17 @@ function handleResult (
|
|
|
376
379
|
return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
|
|
377
380
|
}
|
|
378
381
|
|
|
382
|
+
// Shared stores may outlive the Undici version that wrote them. Do not
|
|
383
|
+
// re-serve a Set-Cookie header from an existing shared-cache entry.
|
|
384
|
+
if (globalOpts.type === 'shared' && Object.hasOwn(result.headers, 'set-cookie')) {
|
|
385
|
+
if (util.isStream(result.body)) {
|
|
386
|
+
result.body.on('error', nop).destroy()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
deleteCachedValue(globalOpts.store, cacheKey)
|
|
390
|
+
return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
|
|
391
|
+
}
|
|
392
|
+
|
|
379
393
|
const now = Date.now()
|
|
380
394
|
if (now > result.deleteAt) {
|
|
381
395
|
// Response is expired, cache store shouldn't have given this to us
|
|
@@ -574,6 +588,11 @@ module.exports = (opts = {}) => {
|
|
|
574
588
|
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
575
589
|
*/
|
|
576
590
|
const cacheKey = makeCacheKey(opts)
|
|
591
|
+
|
|
592
|
+
if (!arrayIncludes(util.safeHTTPMethods, opts.method)) {
|
|
593
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
594
|
+
}
|
|
595
|
+
|
|
577
596
|
const result = store.get(cacheKey)
|
|
578
597
|
|
|
579
598
|
if (result && typeof result.then === 'function') {
|