undici 7.28.0 → 7.29.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/lib/cache/memory-cache-store.js +54 -9
- package/lib/cache/sqlite-cache-store.js +7 -1
- package/lib/core/request.js +12 -1
- package/lib/dispatcher/client-h1.js +11 -2
- package/lib/handler/cache-handler.js +246 -64
- package/lib/handler/cache-revalidation-handler.js +3 -3
- package/lib/handler/retry-handler.js +24 -0
- package/lib/interceptor/cache.js +159 -52
- package/lib/util/cache.js +324 -62
- package/lib/util/date.js +23 -6
- package/lib/web/cookies/util.js +79 -9
- package/package.json +1 -1
|
@@ -218,17 +218,62 @@ class MemoryCacheStore extends EventEmitter {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
function findEntry (key, entries, now) {
|
|
221
|
-
|
|
222
|
-
entry
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
for (let i = 0; i < entries.length; i++) {
|
|
222
|
+
const entry = entries[i]
|
|
223
|
+
if (
|
|
224
|
+
entry.deleteAt > now &&
|
|
225
|
+
entry.method === key.method &&
|
|
226
|
+
varyMatches(key, entry)
|
|
227
|
+
) {
|
|
228
|
+
return entry
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function varyMatches (key, entry) {
|
|
234
|
+
if (entry.vary == null) {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const headerName in entry.vary) {
|
|
239
|
+
if (Object.hasOwn(entry.vary, headerName) && !headerValueEquals(key.headers?.[headerName], entry.vary[headerName])) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {string|string[]|null|undefined} lhs
|
|
249
|
+
* @param {string|string[]|null|undefined} rhs
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
function headerValueEquals (lhs, rhs) {
|
|
253
|
+
if (lhs == null && rhs == null) {
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if ((lhs == null && rhs != null) ||
|
|
258
|
+
(lhs != null && rhs == null)) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (Array.isArray(lhs) && Array.isArray(rhs)) {
|
|
263
|
+
if (lhs.length !== rhs.length) {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
268
|
+
if (lhs[i] !== rhs[i]) {
|
|
269
|
+
return false
|
|
227
270
|
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
228
275
|
|
|
229
|
-
|
|
230
|
-
}))
|
|
231
|
-
))
|
|
276
|
+
return lhs === rhs
|
|
232
277
|
}
|
|
233
278
|
|
|
234
279
|
module.exports = MemoryCacheStore
|
|
@@ -454,7 +454,13 @@ function headerValueEquals (lhs, rhs) {
|
|
|
454
454
|
return false
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
|
|
457
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
458
|
+
if (lhs[i] !== rhs[i]) {
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return true
|
|
458
464
|
}
|
|
459
465
|
|
|
460
466
|
return lhs === rhs
|
package/lib/core/request.js
CHANGED
|
@@ -390,7 +390,13 @@ function processHeader (request, key, val) {
|
|
|
390
390
|
} else if (typeof val[i] === 'object') {
|
|
391
391
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
392
392
|
} else {
|
|
393
|
-
|
|
393
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
394
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
395
|
+
const str = `${val[i]}`
|
|
396
|
+
if (!isValidHeaderValue(str)) {
|
|
397
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
398
|
+
}
|
|
399
|
+
arr.push(str)
|
|
394
400
|
}
|
|
395
401
|
}
|
|
396
402
|
val = arr
|
|
@@ -401,7 +407,12 @@ function processHeader (request, key, val) {
|
|
|
401
407
|
} else if (val === null) {
|
|
402
408
|
val = ''
|
|
403
409
|
} else {
|
|
410
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
411
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
404
412
|
val = `${val}`
|
|
413
|
+
if (!isValidHeaderValue(val)) {
|
|
414
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
415
|
+
}
|
|
405
416
|
}
|
|
406
417
|
|
|
407
418
|
if (headerName === 'host') {
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
RequestContentLengthMismatchError,
|
|
11
11
|
ResponseContentLengthMismatchError,
|
|
12
12
|
RequestAbortedError,
|
|
13
|
+
InvalidArgumentError,
|
|
13
14
|
HeadersTimeoutError,
|
|
14
15
|
HeadersOverflowError,
|
|
15
16
|
SocketError,
|
|
@@ -1134,8 +1135,16 @@ function writeH1 (client, request) {
|
|
|
1134
1135
|
}
|
|
1135
1136
|
body = bodyStream.stream
|
|
1136
1137
|
contentLength = bodyStream.length
|
|
1137
|
-
} else if (util.isBlobLike(body) && request.contentType == null
|
|
1138
|
-
|
|
1138
|
+
} else if (util.isBlobLike(body) && request.contentType == null) {
|
|
1139
|
+
const contentType = body.type
|
|
1140
|
+
if (contentType) {
|
|
1141
|
+
const contentTypeValue = `${contentType}`
|
|
1142
|
+
if (!util.isValidHeaderValue(contentTypeValue)) {
|
|
1143
|
+
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
|
1144
|
+
return false
|
|
1145
|
+
}
|
|
1146
|
+
headers.push('content-type', contentTypeValue)
|
|
1147
|
+
}
|
|
1139
1148
|
}
|
|
1140
1149
|
|
|
1141
1150
|
if (body && typeof body.read === 'function') {
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
4
|
const {
|
|
5
5
|
parseCacheControlHeader,
|
|
6
|
+
hasInvalidCacheControlDirective,
|
|
6
7
|
parseVaryHeader,
|
|
8
|
+
hasVaryStar,
|
|
9
|
+
isInvalidOrWildcardVaryHeader,
|
|
7
10
|
isEtagUsable
|
|
8
11
|
} = require('../util/cache')
|
|
9
12
|
const { parseHttpDate } = require('../util/date.js')
|
|
@@ -26,6 +29,92 @@ const NOT_UNDERSTOOD_STATUS_CODES = [
|
|
|
26
29
|
|
|
27
30
|
const MAX_RESPONSE_AGE = 2147483647000
|
|
28
31
|
|
|
32
|
+
function trimOWS (value) {
|
|
33
|
+
return value.replace(/^[\t ]+|[\t ]+$/g, '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function arrayIncludes (array, value) {
|
|
37
|
+
for (let i = 0; i < array.length; i++) {
|
|
38
|
+
if (array[i] === value) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendConnectionHeaderTokens (headersToRemove, connectionHeader) {
|
|
47
|
+
const values = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader]
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < values.length; i++) {
|
|
50
|
+
const tokens = values[i].split(',')
|
|
51
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
52
|
+
headersToRemove.push(trimOWS(tokens[j]).toLowerCase())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getSameOriginPath (cacheKey, location) {
|
|
58
|
+
if (typeof location !== 'string') {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let originUrl
|
|
63
|
+
let requestUrl
|
|
64
|
+
let locationUrl
|
|
65
|
+
try {
|
|
66
|
+
originUrl = new URL(cacheKey.origin)
|
|
67
|
+
requestUrl = new URL(cacheKey.path, originUrl)
|
|
68
|
+
locationUrl = new URL(location, requestUrl)
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (locationUrl.origin !== originUrl.origin) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return locationUrl.pathname + locationUrl.search
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function deleteCachedUri (store, cacheKey, path) {
|
|
81
|
+
deleteCachedValue(store, {
|
|
82
|
+
...cacheKey,
|
|
83
|
+
path
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < util.safeHTTPMethods.length; i++) {
|
|
87
|
+
const method = util.safeHTTPMethods[i]
|
|
88
|
+
if (method !== cacheKey.method) {
|
|
89
|
+
deleteCachedValue(store, {
|
|
90
|
+
...cacheKey,
|
|
91
|
+
method,
|
|
92
|
+
path
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function deleteLocationTargets (store, cacheKey, headerValue) {
|
|
99
|
+
if (headerValue === undefined) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const values = Array.isArray(headerValue) ? headerValue : [headerValue]
|
|
104
|
+
for (let i = 0; i < values.length; i++) {
|
|
105
|
+
const path = getSameOriginPath(cacheKey, values[i])
|
|
106
|
+
if (path !== undefined) {
|
|
107
|
+
deleteCachedUri(store, cacheKey, path)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function invalidateUnsafeRequest (store, cacheKey, resHeaders) {
|
|
113
|
+
deleteCachedUri(store, cacheKey, cacheKey.path)
|
|
114
|
+
deleteLocationTargets(store, cacheKey, resHeaders.location)
|
|
115
|
+
deleteLocationTargets(store, cacheKey, resHeaders['content-location'])
|
|
116
|
+
}
|
|
117
|
+
|
|
29
118
|
/**
|
|
30
119
|
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
|
|
31
120
|
*
|
|
@@ -107,28 +196,28 @@ class CacheHandler {
|
|
|
107
196
|
const handler = this
|
|
108
197
|
|
|
109
198
|
if (
|
|
110
|
-
!util.safeHTTPMethods
|
|
199
|
+
!arrayIncludes(util.safeHTTPMethods, this.#cacheKey.method) &&
|
|
111
200
|
statusCode >= 200 &&
|
|
112
201
|
statusCode <= 399
|
|
113
202
|
) {
|
|
114
203
|
// Successful response to an unsafe method, delete it from cache
|
|
115
204
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
|
|
116
|
-
|
|
117
|
-
this.#store.delete(this.#cacheKey)?.catch?.(noop)
|
|
118
|
-
} catch {
|
|
119
|
-
// Fail silently
|
|
120
|
-
}
|
|
205
|
+
invalidateUnsafeRequest(this.#store, this.#cacheKey, resHeaders)
|
|
121
206
|
return downstreamOnHeaders()
|
|
122
207
|
}
|
|
123
208
|
|
|
124
209
|
const cacheControlHeader = resHeaders['cache-control']
|
|
125
|
-
const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES
|
|
210
|
+
const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
|
|
126
211
|
if (
|
|
127
212
|
!cacheControlHeader &&
|
|
128
213
|
!resHeaders['expires'] &&
|
|
129
214
|
!heuristicallyCacheable &&
|
|
130
215
|
!this.#cacheByDefault
|
|
131
216
|
) {
|
|
217
|
+
if (statusCode === 304 && resHeaders.vary && isInvalidOrWildcardVaryHeader(resHeaders.vary)) {
|
|
218
|
+
deleteCachedValue(this.#store, this.#cacheKey)
|
|
219
|
+
}
|
|
220
|
+
|
|
132
221
|
// Don't have anything to tell us this response is cachable and we're not
|
|
133
222
|
// caching by default
|
|
134
223
|
return downstreamOnHeaders()
|
|
@@ -136,31 +225,46 @@ class CacheHandler {
|
|
|
136
225
|
|
|
137
226
|
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
138
227
|
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
|
|
228
|
+
if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
|
|
229
|
+
deleteCachedValue(this.#store, this.#cacheKey)
|
|
230
|
+
}
|
|
231
|
+
|
|
139
232
|
return downstreamOnHeaders()
|
|
140
233
|
}
|
|
141
234
|
|
|
142
235
|
const now = Date.now()
|
|
143
|
-
const resAge = resHeaders
|
|
144
|
-
if (resAge && resAge >= MAX_RESPONSE_AGE) {
|
|
236
|
+
const resAge = Object.hasOwn(resHeaders, 'age') ? getAge(resHeaders.age) : undefined
|
|
237
|
+
if (resAge !== undefined && resAge >= MAX_RESPONSE_AGE) {
|
|
145
238
|
// Response considered stale
|
|
239
|
+
deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
|
|
146
240
|
return downstreamOnHeaders()
|
|
147
241
|
}
|
|
148
242
|
|
|
149
|
-
const resDate =
|
|
150
|
-
|
|
151
|
-
|
|
243
|
+
const resDate = Object.hasOwn(resHeaders, 'date') ? getDate(resHeaders.date) : undefined
|
|
244
|
+
if (resDate === null) {
|
|
245
|
+
deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
|
|
246
|
+
return downstreamOnHeaders()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const apparentAge = resDate ? Math.max(0, now - resDate.getTime()) : 0
|
|
250
|
+
const currentAge = Math.max(apparentAge, resAge ?? 0)
|
|
152
251
|
|
|
153
252
|
const staleAt =
|
|
154
253
|
determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
|
|
155
254
|
this.#cacheByDefault
|
|
156
|
-
if (staleAt === undefined ||
|
|
255
|
+
if (staleAt === undefined || currentAge >= staleAt) {
|
|
256
|
+
if (cacheControlHeader || staleAt !== undefined) {
|
|
257
|
+
deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
|
|
258
|
+
}
|
|
259
|
+
|
|
157
260
|
return downstreamOnHeaders()
|
|
158
261
|
}
|
|
159
262
|
|
|
160
|
-
const baseTime =
|
|
263
|
+
const baseTime = now - currentAge
|
|
161
264
|
const absoluteStaleAt = staleAt + baseTime
|
|
162
265
|
if (now >= absoluteStaleAt) {
|
|
163
266
|
// Response is already stale
|
|
267
|
+
deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
|
|
164
268
|
return downstreamOnHeaders()
|
|
165
269
|
}
|
|
166
270
|
|
|
@@ -173,7 +277,8 @@ class CacheHandler {
|
|
|
173
277
|
}
|
|
174
278
|
}
|
|
175
279
|
|
|
176
|
-
const
|
|
280
|
+
const cachedAt = baseTime
|
|
281
|
+
const deleteAt = determineDeleteAt(baseTime, now, cacheControlDirectives, absoluteStaleAt)
|
|
177
282
|
const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
|
|
178
283
|
|
|
179
284
|
/**
|
|
@@ -185,7 +290,7 @@ class CacheHandler {
|
|
|
185
290
|
headers: strippedHeaders,
|
|
186
291
|
vary: varyDirectives,
|
|
187
292
|
cacheControlDirectives,
|
|
188
|
-
cachedAt
|
|
293
|
+
cachedAt,
|
|
189
294
|
staleAt: absoluteStaleAt,
|
|
190
295
|
deleteAt
|
|
191
296
|
}
|
|
@@ -203,6 +308,7 @@ class CacheHandler {
|
|
|
203
308
|
value.statusCode = cachedValue.statusCode
|
|
204
309
|
value.statusMessage = cachedValue.statusMessage
|
|
205
310
|
value.etag = cachedValue.etag
|
|
311
|
+
value.vary = varyDirectives ?? cachedValue.vary
|
|
206
312
|
value.headers = { ...cachedValue.headers, ...strippedHeaders }
|
|
207
313
|
|
|
208
314
|
downstreamOnHeaders()
|
|
@@ -333,6 +439,36 @@ class CacheHandler {
|
|
|
333
439
|
}
|
|
334
440
|
}
|
|
335
441
|
|
|
442
|
+
/**
|
|
443
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStore} store
|
|
444
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
445
|
+
*/
|
|
446
|
+
function deleteCachedValue (store, cacheKey) {
|
|
447
|
+
try {
|
|
448
|
+
store.delete(cacheKey)?.catch?.(noop)
|
|
449
|
+
} catch {
|
|
450
|
+
// Fail silently
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
|
|
455
|
+
if (statusCode === 304) {
|
|
456
|
+
deleteCachedValue(store, cacheKey)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
462
|
+
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
463
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
464
|
+
* @returns {boolean}
|
|
465
|
+
*/
|
|
466
|
+
function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
|
|
467
|
+
return cacheControlDirectives['no-store'] === true ||
|
|
468
|
+
(cacheType === 'shared' && cacheControlDirectives.private === true) ||
|
|
469
|
+
(resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
|
|
470
|
+
}
|
|
471
|
+
|
|
336
472
|
/**
|
|
337
473
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
338
474
|
*
|
|
@@ -344,12 +480,12 @@ class CacheHandler {
|
|
|
344
480
|
*/
|
|
345
481
|
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
|
|
346
482
|
// Status code must be final and understood.
|
|
347
|
-
if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES
|
|
483
|
+
if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
|
|
348
484
|
return false
|
|
349
485
|
}
|
|
350
486
|
// Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
|
|
351
487
|
// directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
|
|
352
|
-
if (!HEURISTICALLY_CACHEABLE_STATUS_CODES
|
|
488
|
+
if (!arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) && !resHeaders['expires'] &&
|
|
353
489
|
!cacheControlDirectives.public &&
|
|
354
490
|
cacheControlDirectives['max-age'] === undefined &&
|
|
355
491
|
// RFC 9111: a private response directive, if the cache is not shared
|
|
@@ -368,12 +504,12 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
|
|
|
368
504
|
}
|
|
369
505
|
|
|
370
506
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
|
|
371
|
-
if (resHeaders.vary
|
|
507
|
+
if (resHeaders.vary && hasVaryStar(resHeaders.vary)) {
|
|
372
508
|
return false
|
|
373
509
|
}
|
|
374
510
|
|
|
375
511
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
376
|
-
if (reqHeaders
|
|
512
|
+
if (reqHeaders != null && Object.hasOwn(reqHeaders, 'authorization')) {
|
|
377
513
|
if (
|
|
378
514
|
!cacheControlDirectives.public &&
|
|
379
515
|
!cacheControlDirectives['s-maxage'] &&
|
|
@@ -388,14 +524,14 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
|
|
|
388
524
|
|
|
389
525
|
if (
|
|
390
526
|
Array.isArray(cacheControlDirectives['no-cache']) &&
|
|
391
|
-
cacheControlDirectives['no-cache']
|
|
527
|
+
arrayIncludes(cacheControlDirectives['no-cache'], 'authorization')
|
|
392
528
|
) {
|
|
393
529
|
return false
|
|
394
530
|
}
|
|
395
531
|
|
|
396
532
|
if (
|
|
397
533
|
Array.isArray(cacheControlDirectives['private']) &&
|
|
398
|
-
cacheControlDirectives['private']
|
|
534
|
+
arrayIncludes(cacheControlDirectives['private'], 'authorization')
|
|
399
535
|
) {
|
|
400
536
|
return false
|
|
401
537
|
}
|
|
@@ -404,14 +540,51 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
|
|
|
404
540
|
return true
|
|
405
541
|
}
|
|
406
542
|
|
|
543
|
+
/**
|
|
544
|
+
* @param {string | string[]} dateHeader
|
|
545
|
+
* @returns {Date | null | undefined}
|
|
546
|
+
*/
|
|
547
|
+
function getDate (dateHeader) {
|
|
548
|
+
let dateValue = dateHeader
|
|
549
|
+
if (Array.isArray(dateValue)) {
|
|
550
|
+
if (dateValue.length !== 1) {
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
dateValue = dateValue[0]
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (typeof dateValue !== 'string') {
|
|
558
|
+
return null
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return parseHttpDate(dateValue)
|
|
562
|
+
}
|
|
563
|
+
|
|
407
564
|
/**
|
|
408
565
|
* @param {string | string[]} ageHeader
|
|
409
566
|
* @returns {number | undefined}
|
|
410
567
|
*/
|
|
411
568
|
function getAge (ageHeader) {
|
|
412
|
-
|
|
569
|
+
let ageValue = ageHeader
|
|
570
|
+
if (Array.isArray(ageValue)) {
|
|
571
|
+
if (ageValue.length !== 1) {
|
|
572
|
+
return MAX_RESPONSE_AGE
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
ageValue = ageValue[0]
|
|
576
|
+
}
|
|
413
577
|
|
|
414
|
-
|
|
578
|
+
if (typeof ageValue !== 'string' || !/^[\t ]*[0-9]+[\t ]*$/.test(ageValue)) {
|
|
579
|
+
return MAX_RESPONSE_AGE
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const age = BigInt(ageValue.replace(/^[\t ]+|[\t ]+$/g, ''))
|
|
583
|
+
if (age >= BigInt(MAX_RESPONSE_AGE / 1000)) {
|
|
584
|
+
return MAX_RESPONSE_AGE
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return Number(age) * 1000
|
|
415
588
|
}
|
|
416
589
|
|
|
417
590
|
/**
|
|
@@ -429,43 +602,60 @@ function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheC
|
|
|
429
602
|
// Prioritize s-maxage since we're a shared cache
|
|
430
603
|
// s-maxage > max-age > Expire
|
|
431
604
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
|
|
605
|
+
if (hasInvalidCacheControlDirective(cacheControlDirectives, 's-maxage')) {
|
|
606
|
+
return 0
|
|
607
|
+
}
|
|
608
|
+
|
|
432
609
|
const sMaxAge = cacheControlDirectives['s-maxage']
|
|
433
610
|
if (sMaxAge !== undefined) {
|
|
434
|
-
return sMaxAge
|
|
611
|
+
return sMaxAge * 1000
|
|
435
612
|
}
|
|
436
613
|
}
|
|
437
614
|
|
|
615
|
+
if (hasInvalidCacheControlDirective(cacheControlDirectives, 'max-age')) {
|
|
616
|
+
return 0
|
|
617
|
+
}
|
|
618
|
+
|
|
438
619
|
const maxAge = cacheControlDirectives['max-age']
|
|
439
620
|
if (maxAge !== undefined) {
|
|
440
|
-
return maxAge
|
|
621
|
+
return maxAge * 1000
|
|
441
622
|
}
|
|
442
623
|
|
|
443
|
-
if (
|
|
624
|
+
if (Object.hasOwn(resHeaders, 'expires')) {
|
|
444
625
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
626
|
+
if (typeof resHeaders.expires !== 'string') {
|
|
627
|
+
return 0
|
|
628
|
+
}
|
|
629
|
+
|
|
445
630
|
const expiresDate = parseHttpDate(resHeaders.expires)
|
|
446
|
-
if (expiresDate) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
631
|
+
if (!expiresDate) {
|
|
632
|
+
return 0
|
|
633
|
+
}
|
|
450
634
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
635
|
+
if (now >= expiresDate.getTime()) {
|
|
636
|
+
return 0
|
|
637
|
+
}
|
|
455
638
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
639
|
+
if (responseDate) {
|
|
640
|
+
if (responseDate >= expiresDate) {
|
|
641
|
+
return 0
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const freshnessLifetime = expiresDate.getTime() - responseDate.getTime()
|
|
645
|
+
if (age !== undefined && age >= freshnessLifetime) {
|
|
646
|
+
return 0
|
|
459
647
|
}
|
|
460
648
|
|
|
461
|
-
return
|
|
649
|
+
return freshnessLifetime
|
|
462
650
|
}
|
|
651
|
+
|
|
652
|
+
return expiresDate.getTime() - now
|
|
463
653
|
}
|
|
464
654
|
|
|
465
655
|
if (typeof resHeaders['last-modified'] === 'string') {
|
|
466
656
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
|
|
467
|
-
const lastModified =
|
|
468
|
-
if (
|
|
657
|
+
const lastModified = parseHttpDate(resHeaders['last-modified'])
|
|
658
|
+
if (lastModified) {
|
|
469
659
|
if (lastModified.getTime() >= now) {
|
|
470
660
|
return undefined
|
|
471
661
|
}
|
|
@@ -478,18 +668,19 @@ function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheC
|
|
|
478
668
|
|
|
479
669
|
if (cacheControlDirectives.immutable) {
|
|
480
670
|
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
481
|
-
return
|
|
671
|
+
return 31536000000
|
|
482
672
|
}
|
|
483
673
|
|
|
484
674
|
return undefined
|
|
485
675
|
}
|
|
486
676
|
|
|
487
677
|
/**
|
|
488
|
-
* @param {number}
|
|
678
|
+
* @param {number} baseTime
|
|
679
|
+
* @param {number} cachedAt
|
|
489
680
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
490
681
|
* @param {number} staleAt
|
|
491
682
|
*/
|
|
492
|
-
function determineDeleteAt (
|
|
683
|
+
function determineDeleteAt (baseTime, cachedAt, cacheControlDirectives, staleAt) {
|
|
493
684
|
let staleWhileRevalidate = -Infinity
|
|
494
685
|
let staleIfError = -Infinity
|
|
495
686
|
let immutable = -Infinity
|
|
@@ -503,15 +694,21 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
|
503
694
|
}
|
|
504
695
|
|
|
505
696
|
if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
506
|
-
immutable =
|
|
697
|
+
immutable = cachedAt + 31536000000
|
|
507
698
|
}
|
|
508
699
|
|
|
509
700
|
// When no stale directives or immutable flag, add a revalidation buffer
|
|
510
701
|
// equal to the freshness lifetime so the entry survives past staleAt long
|
|
511
702
|
// enough to be revalidated instead of silently disappearing.
|
|
703
|
+
//
|
|
704
|
+
// Response Date headers only have second precision, so baseTime can trail the
|
|
705
|
+
// actual cache insertion time by up to ~1s. Pad the buffer by that bounded
|
|
706
|
+
// skew so short-lived entries do not disappear exactly when they should be
|
|
707
|
+
// revalidated.
|
|
512
708
|
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
|
|
513
|
-
const freshnessLifetime = staleAt -
|
|
514
|
-
|
|
709
|
+
const freshnessLifetime = staleAt - baseTime
|
|
710
|
+
const datePrecisionPadding = Math.min(Math.max(cachedAt - baseTime, 0), 1000)
|
|
711
|
+
return staleAt + freshnessLifetime + datePrecisionPadding
|
|
515
712
|
}
|
|
516
713
|
|
|
517
714
|
return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
|
|
@@ -538,14 +735,7 @@ function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
|
|
|
538
735
|
]
|
|
539
736
|
|
|
540
737
|
if (resHeaders['connection']) {
|
|
541
|
-
|
|
542
|
-
// connection: a
|
|
543
|
-
// connection: b
|
|
544
|
-
headersToRemove.push(...resHeaders['connection'].map(header => header.trim()))
|
|
545
|
-
} else {
|
|
546
|
-
// connection: a, b
|
|
547
|
-
headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim()))
|
|
548
|
-
}
|
|
738
|
+
appendConnectionHeaderTokens(headersToRemove, resHeaders['connection'])
|
|
549
739
|
}
|
|
550
740
|
|
|
551
741
|
if (Array.isArray(cacheControlDirectives['no-cache'])) {
|
|
@@ -558,7 +748,7 @@ function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
|
|
|
558
748
|
|
|
559
749
|
let strippedHeaders
|
|
560
750
|
for (const headerName of headersToRemove) {
|
|
561
|
-
if (resHeaders
|
|
751
|
+
if (Object.hasOwn(resHeaders, headerName)) {
|
|
562
752
|
strippedHeaders ??= { ...resHeaders }
|
|
563
753
|
delete strippedHeaders[headerName]
|
|
564
754
|
}
|
|
@@ -567,12 +757,4 @@ function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
|
|
|
567
757
|
return strippedHeaders ?? resHeaders
|
|
568
758
|
}
|
|
569
759
|
|
|
570
|
-
/**
|
|
571
|
-
* @param {Date} date
|
|
572
|
-
* @returns {boolean}
|
|
573
|
-
*/
|
|
574
|
-
function isValidDate (date) {
|
|
575
|
-
return date instanceof Date && Number.isFinite(date.valueOf())
|
|
576
|
-
}
|
|
577
|
-
|
|
578
760
|
module.exports = CacheHandler
|
|
@@ -19,7 +19,7 @@ class CacheRevalidationHandler {
|
|
|
19
19
|
#successful = false
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* @type {((boolean, any) => void) | null}
|
|
22
|
+
* @type {((success: boolean, context?: any, statusCode?: number, headers?: import('../../types/header.d.ts').IncomingHttpHeaders) => void) | null}
|
|
23
23
|
*/
|
|
24
24
|
#callback
|
|
25
25
|
|
|
@@ -36,7 +36,7 @@ class CacheRevalidationHandler {
|
|
|
36
36
|
#allowErrorStatusCodes
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* @param {(boolean) => void} callback Function to call if the cached value is valid
|
|
39
|
+
* @param {(success: boolean, context?: any, statusCode?: number, headers?: import('../../types/header.d.ts').IncomingHttpHeaders) => void} callback Function to call if the cached value is valid
|
|
40
40
|
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
41
41
|
* @param {boolean} allowErrorStatusCodes
|
|
42
42
|
*/
|
|
@@ -71,7 +71,7 @@ class CacheRevalidationHandler {
|
|
|
71
71
|
// https://datatracker.ietf.org/doc/html/rfc5861#section-4
|
|
72
72
|
this.#successful = statusCode === 304 ||
|
|
73
73
|
(this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
|
|
74
|
-
this.#callback(this.#successful, this.#context)
|
|
74
|
+
this.#callback(this.#successful, this.#context, statusCode, headers)
|
|
75
75
|
this.#callback = null
|
|
76
76
|
|
|
77
77
|
if (this.#successful) {
|