@@ -4,10 +4,74 @@ const util = require('../core/util')
4
4
const CacheHandler = require ( '../handler/cache-handler' )
5
5
const MemoryCacheStore = require ( '../cache/memory-cache-store' )
6
6
const CacheRevalidationHandler = require ( '../handler/cache-revalidation-handler' )
7
- const { UNSAFE_METHODS , assertCacheStoreType } = require ( '../util/cache.js' )
7
+ const {
8
+ UNSAFE_METHODS ,
9
+ assertCacheStoreType,
10
+ parseCacheControlHeader
11
+ } = require ( '../util/cache.js' )
8
12
9
13
const AGE_HEADER = Buffer . from ( 'age' )
10
14
15
+ /**
16
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers } handler
17
+ */
18
+ function sendGatewayTimeout ( handler ) {
19
+ const ac = new AbortController ( )
20
+ const signal = ac . signal
21
+
22
+ try {
23
+ if ( typeof handler . onConnect === 'function' ) {
24
+ handler . onConnect ( ac . abort )
25
+ signal . throwIfAborted ( )
26
+ }
27
+
28
+ if ( typeof handler . onHeaders === 'function' ) {
29
+ handler . onHeaders ( 504 , [ ] , ( ) => { } , 'Gateway Timeout' )
30
+ signal . throwIfAborted ( )
31
+ }
32
+
33
+ if ( typeof handler . onComplete === 'function' ) {
34
+ handler . onComplete ( [ ] )
35
+ }
36
+ } catch ( err ) {
37
+ if ( typeof handler . onError === 'function' ) {
38
+ handler . onError ( err )
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @param {number } now
45
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue } value
46
+ * @param {number } age
47
+ * @param {import('../util/cache.js').CacheControlDirectives } cacheControlDirectives
48
+ */
49
+ function needsRevalidation ( now , value , age , cacheControlDirectives ) {
50
+ if ( cacheControlDirectives ?. [ 'no-cache' ] ) {
51
+ // Always revalidate requests with the no-cache parameter
52
+ return true
53
+ }
54
+
55
+ if ( now > value . staleAt ) {
56
+ // Response is stale
57
+ if ( cacheControlDirectives ?. [ 'max-stale' ] ) {
58
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
59
+ const gracePeriod = value . staleAt + ( cacheControlDirectives [ 'max-stale' ] * 1000 )
60
+ return now > gracePeriod
61
+ }
62
+
63
+ return true
64
+ }
65
+
66
+ if ( cacheControlDirectives ?. [ 'min-fresh' ] ) {
67
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
68
+ const gracePeriod = age + ( cacheControlDirectives [ 'min-fresh' ] * 1000 )
69
+ return ( now - value . staleAt ) > gracePeriod
70
+ }
71
+
72
+ return false
73
+ }
74
+
11
75
/**
12
76
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions | undefined } globalOpts
13
77
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor }
@@ -45,9 +109,25 @@ module.exports = globalOpts => {
45
109
return dispatch ( opts , handler )
46
110
}
47
111
112
+ const requestCacheControl = opts . headers ?. [ 'cache-control' ]
113
+ ? parseCacheControlHeader ( opts . headers [ 'cache-control' ] )
114
+ : undefined
115
+
116
+ if ( requestCacheControl ?. [ 'no-store' ] ) {
117
+ return dispatch ( opts , handler )
118
+ }
119
+
48
120
const stream = globalOpts . store . createReadStream ( opts )
49
121
if ( ! stream ) {
50
122
// Request isn't cached
123
+
124
+ if ( requestCacheControl ?. [ 'only-if-cached' ] ) {
125
+ // We only want cached responses
126
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
127
+ sendGatewayTimeout ( handler )
128
+ return true
129
+ }
130
+
51
131
return dispatch ( opts , new CacheHandler ( globalOpts , opts , handler ) )
52
132
}
53
133
@@ -128,19 +208,35 @@ module.exports = globalOpts => {
128
208
const handleStream = ( stream ) => {
129
209
if ( ! stream ) {
130
210
// Request isn't cached
211
+
212
+ if ( requestCacheControl ?. [ 'only-if-cached' ] ) {
213
+ // We only want cached responses
214
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
215
+ sendGatewayTimeout ( handler )
216
+ return
217
+ }
218
+
131
219
return dispatch ( opts , new CacheHandler ( globalOpts , opts , handler ) )
132
220
}
133
221
134
222
const { value } = stream
135
223
224
+ const now = Date . now ( )
225
+ const age = Math . round ( ( now - value . cachedAt ) / 1000 )
226
+ if ( requestCacheControl ?. [ 'max-age' ] && age >= requestCacheControl [ 'max-age' ] ) {
227
+ // Response is considered expired for this specific request
228
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
229
+ dispatch ( opts , handler )
230
+ return
231
+ }
232
+
136
233
// Dump body on error
137
234
if ( util . isStream ( opts . body ) ) {
138
235
opts . body ?. on ( 'error' , ( ) => { } ) . resume ( )
139
236
}
140
237
141
238
// Check if the response is stale
142
- const now = Date . now ( )
143
- if ( now >= value . staleAt ) {
239
+ if ( needsRevalidation ( now , value , age , requestCacheControl ) ) {
144
240
if ( now >= value . deleteAt ) {
145
241
// Safety check in case the store gave us a response that should've been
146
242
// deleted already
0 commit comments