@@ -6,14 +6,82 @@ const util = require('../core/util')
6
6
const CacheHandler = require ( '../handler/cache-handler' )
7
7
const MemoryCacheStore = require ( '../cache/memory-cache-store' )
8
8
const CacheRevalidationHandler = require ( '../handler/cache-revalidation-handler' )
9
- const { assertCacheStore, assertCacheMethods, makeCacheKey } = require ( '../util/cache.js' )
9
+ const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require ( '../util/cache.js' )
10
10
const { nowAbsolute } = require ( '../util/timers.js' )
11
11
12
12
const AGE_HEADER = Buffer . from ( 'age' )
13
13
14
14
/**
15
- * @typedef {import('../../types/cache-interceptor .d.ts').default.CachedResponse } CachedResponse
15
+ * @param {import('../../types/dispatcher .d.ts').default.DispatchHandlers } handler
16
16
*/
17
+ function sendGatewayTimeout ( handler ) {
18
+ let aborted = false
19
+ try {
20
+ if ( typeof handler . onConnect === 'function' ) {
21
+ handler . onConnect ( ( ) => {
22
+ aborted = true
23
+ } )
24
+
25
+ if ( aborted ) {
26
+ return
27
+ }
28
+ }
29
+
30
+ if ( typeof handler . onHeaders === 'function' ) {
31
+ handler . onHeaders ( 504 , [ ] , ( ) => { } , 'Gateway Timeout' )
32
+ if ( aborted ) {
33
+ return
34
+ }
35
+ }
36
+
37
+ if ( typeof handler . onComplete === 'function' ) {
38
+ handler . onComplete ( [ ] )
39
+ }
40
+ } catch ( err ) {
41
+ if ( typeof handler . onError === 'function' ) {
42
+ handler . onError ( err )
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult } result
49
+ * @param {number } age
50
+ * @param {import('../util/cache.js').CacheControlDirectives | undefined } cacheControlDirectives
51
+ * @returns {boolean }
52
+ */
53
+ function needsRevalidation ( result , age , cacheControlDirectives ) {
54
+ if ( cacheControlDirectives ?. [ 'no-cache' ] ) {
55
+ // Always revalidate requests with the no-cache directive
56
+ return true
57
+ }
58
+
59
+ const now = nowAbsolute ( )
60
+ if ( now > result . staleAt ) {
61
+ // Response is stale
62
+ if ( cacheControlDirectives ?. [ 'max-stale' ] ) {
63
+ // There's a threshold where we can serve stale responses, let's see if
64
+ // we're in it
65
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
66
+ const gracePeriod = result . staleAt + ( cacheControlDirectives [ 'max-stale' ] * 1000 )
67
+ return now > gracePeriod
68
+ }
69
+
70
+ return true
71
+ }
72
+
73
+ if ( cacheControlDirectives ?. [ 'min-fresh' ] ) {
74
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
75
+
76
+ // At this point, staleAt is always > now
77
+ const timeLeftTillStale = result . staleAt - now
78
+ const threshold = cacheControlDirectives [ 'min-fresh' ] * 1000
79
+
80
+ return timeLeftTillStale <= threshold
81
+ }
82
+
83
+ return false
84
+ }
17
85
18
86
/**
19
87
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions } [opts]
@@ -49,6 +117,14 @@ module.exports = (opts = {}) => {
49
117
return dispatch ( opts , handler )
50
118
}
51
119
120
+ const requestCacheControl = opts . headers ?. [ 'cache-control' ]
121
+ ? parseCacheControlHeader ( opts . headers [ 'cache-control' ] )
122
+ : undefined
123
+
124
+ if ( requestCacheControl ?. [ 'no-store' ] ) {
125
+ return dispatch ( opts , handler )
126
+ }
127
+
52
128
/**
53
129
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey }
54
130
*/
@@ -59,13 +135,21 @@ module.exports = (opts = {}) => {
59
135
// Where body can be a Buffer, string, stream or blob?
60
136
const result = store . get ( cacheKey )
61
137
if ( ! result ) {
138
+ if ( requestCacheControl ?. [ 'only-if-cached' ] ) {
139
+ // We only want cached responses
140
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
141
+ sendGatewayTimeout ( handler )
142
+ return true
143
+ }
144
+
62
145
return dispatch ( opts , new CacheHandler ( globalOpts , cacheKey , handler ) )
63
146
}
64
147
65
148
/**
66
149
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult } result
150
+ * @param {number } age
67
151
*/
68
- const respondWithCachedValue = ( { cachedAt , rawHeaders, statusCode, statusMessage, body } ) => {
152
+ const respondWithCachedValue = ( { rawHeaders, statusCode, statusMessage, body } , age ) => {
69
153
const stream = util . isStream ( body )
70
154
? body
71
155
: Readable . from ( body ?? [ ] )
@@ -102,7 +186,6 @@ module.exports = (opts = {}) => {
102
186
if ( typeof handler . onHeaders === 'function' ) {
103
187
// Add the age header
104
188
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
105
- const age = Math . round ( ( nowAbsolute ( ) - cachedAt ) / 1000 )
106
189
107
190
// TODO (fix): What if rawHeaders already contains age header?
108
191
rawHeaders = [ ...rawHeaders , AGE_HEADER , Buffer . from ( `${ age } ` ) ]
@@ -133,21 +216,23 @@ module.exports = (opts = {}) => {
133
216
throw new Error ( 'stream is undefined but method isn\'t HEAD' )
134
217
}
135
218
219
+ const age = Math . round ( ( nowAbsolute ( ) - result . cachedAt ) / 1000 )
220
+ if ( requestCacheControl ?. [ 'max-age' ] && age >= requestCacheControl [ 'max-age' ] ) {
221
+ // Response is considered expired for this specific request
222
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
223
+ return dispatch ( opts , handler )
224
+ }
225
+
136
226
// Check if the response is stale
137
- const now = nowAbsolute ( )
138
- if ( now < result . staleAt ) {
139
- // Dump request body.
140
- if ( util . isStream ( opts . body ) ) {
141
- opts . body . on ( 'error' , ( ) => { } ) . destroy ( )
227
+ if ( needsRevalidation ( result , age , requestCacheControl ) ) {
228
+ if ( util . isStream ( opts . body ) && util . bodyLength ( opts . body ) !== 0 ) {
229
+ // If body is is stream we can't revalidate.. .
230
+ // TODO (fix): This could be less strict...
231
+ return dispatch ( opts , new CacheHandler ( globalOpts , cacheKey , handler ) )
142
232
}
143
- respondWithCachedValue ( result )
144
- } else if ( util . isStream ( opts . body ) && util . bodyLength ( opts . body ) !== 0 ) {
145
- // If body is is stream we can't revalidate...
146
- // TODO (fix): This could be less strict...
147
- dispatch ( opts , new CacheHandler ( globalOpts , cacheKey , handler ) )
148
- } else {
149
- // Need to revalidate the response
150
- dispatch (
233
+
234
+ // We need to revalidate the response
235
+ return dispatch (
151
236
{
152
237
...opts ,
153
238
headers : {
@@ -159,7 +244,7 @@ module.exports = (opts = {}) => {
159
244
new CacheRevalidationHandler (
160
245
( success ) => {
161
246
if ( success ) {
162
- respondWithCachedValue ( result )
247
+ respondWithCachedValue ( result , age )
163
248
} else if ( util . isStream ( result . body ) ) {
164
249
result . body . on ( 'error' , ( ) => { } ) . destroy ( )
165
250
}
@@ -168,11 +253,24 @@ module.exports = (opts = {}) => {
168
253
)
169
254
)
170
255
}
256
+
257
+ // Dump request body.
258
+ if ( util . isStream ( opts . body ) ) {
259
+ opts . body . on ( 'error' , ( ) => { } ) . destroy ( )
260
+ }
261
+ respondWithCachedValue ( result , age )
171
262
}
172
263
173
264
if ( typeof result . then === 'function' ) {
174
265
result . then ( ( result ) => {
175
266
if ( ! result ) {
267
+ if ( requestCacheControl ?. [ 'only-if-cached' ] ) {
268
+ // We only want cached responses
269
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
270
+ sendGatewayTimeout ( handler )
271
+ return true
272
+ }
273
+
176
274
dispatch ( opts , new CacheHandler ( globalOpts , cacheKey , handler ) )
177
275
} else {
178
276
handleResult ( result )
0 commit comments