@@ -6,14 +6,82 @@ const util = require('../core/util')
66const CacheHandler = require ( '../handler/cache-handler' )
77const MemoryCacheStore = require ( '../cache/memory-cache-store' )
88const 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' )
1010const { nowAbsolute } = require ( '../util/timers.js' )
1111
1212const AGE_HEADER = Buffer . from ( 'age' )
1313
1414/**
15- * @typedef {import('../../types/cache-interceptor .d.ts').default.CachedResponse } CachedResponse
15+ * @param {import('../../types/dispatcher .d.ts').default.DispatchHandlers } handler
1616 */
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+ }
1785
1886/**
1987 * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions } [opts]
@@ -49,6 +117,14 @@ module.exports = (opts = {}) => {
49117 return dispatch ( opts , handler )
50118 }
51119
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+
52128 /**
53129 * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey }
54130 */
@@ -59,13 +135,21 @@ module.exports = (opts = {}) => {
59135 // Where body can be a Buffer, string, stream or blob?
60136 const result = store . get ( cacheKey )
61137 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+
62145 return dispatch ( opts , new CacheHandler ( globalOpts , cacheKey , handler ) )
63146 }
64147
65148 /**
66149 * @param {import('../../types/cache-interceptor.d.ts').default.GetResult } result
150+ * @param {number } age
67151 */
68- const respondWithCachedValue = ( { cachedAt , rawHeaders, statusCode, statusMessage, body } ) => {
152+ const respondWithCachedValue = ( { rawHeaders, statusCode, statusMessage, body } , age ) => {
69153 const stream = util . isStream ( body )
70154 ? body
71155 : Readable . from ( body ?? [ ] )
@@ -102,7 +186,6 @@ module.exports = (opts = {}) => {
102186 if ( typeof handler . onHeaders === 'function' ) {
103187 // Add the age header
104188 // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
105- const age = Math . round ( ( nowAbsolute ( ) - cachedAt ) / 1000 )
106189
107190 // TODO (fix): What if rawHeaders already contains age header?
108191 rawHeaders = [ ...rawHeaders , AGE_HEADER , Buffer . from ( `${ age } ` ) ]
@@ -133,21 +216,23 @@ module.exports = (opts = {}) => {
133216 throw new Error ( 'stream is undefined but method isn\'t HEAD' )
134217 }
135218
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+
136226 // 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 ) )
142232 }
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 (
151236 {
152237 ...opts ,
153238 headers : {
@@ -159,7 +244,7 @@ module.exports = (opts = {}) => {
159244 new CacheRevalidationHandler (
160245 ( success ) => {
161246 if ( success ) {
162- respondWithCachedValue ( result )
247+ respondWithCachedValue ( result , age )
163248 } else if ( util . isStream ( result . body ) ) {
164249 result . body . on ( 'error' , ( ) => { } ) . destroy ( )
165250 }
@@ -168,11 +253,24 @@ module.exports = (opts = {}) => {
168253 )
169254 )
170255 }
256+
257+ // Dump request body.
258+ if ( util . isStream ( opts . body ) ) {
259+ opts . body . on ( 'error' , ( ) => { } ) . destroy ( )
260+ }
261+ respondWithCachedValue ( result , age )
171262 }
172263
173264 if ( typeof result . then === 'function' ) {
174265 result . then ( ( result ) => {
175266 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+
176274 dispatch ( opts , new CacheHandler ( globalOpts , cacheKey , handler ) )
177275 } else {
178276 handleResult ( result )
0 commit comments