Skip to content

Commit 9a15ece

Browse files
committed
feat: support request cache control directives
Signed-off-by: flakey5 <[email protected]>
1 parent b6952b4 commit 9a15ece

File tree

1 file changed

+99
-3
lines changed

1 file changed

+99
-3
lines changed

lib/interceptor/cache.js

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,74 @@ const util = require('../core/util')
44
const CacheHandler = require('../handler/cache-handler')
55
const MemoryCacheStore = require('../cache/memory-cache-store')
66
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')
812

913
const AGE_HEADER = Buffer.from('age')
1014

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+
1175
/**
1276
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions | undefined} globalOpts
1377
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
@@ -45,9 +109,25 @@ module.exports = globalOpts => {
45109
return dispatch(opts, handler)
46110
}
47111

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+
48120
const stream = globalOpts.store.createReadStream(opts)
49121
if (!stream) {
50122
// 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+
51131
return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
52132
}
53133

@@ -128,19 +208,35 @@ module.exports = globalOpts => {
128208
const handleStream = (stream) => {
129209
if (!stream) {
130210
// 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+
131219
return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
132220
}
133221

134222
const { value } = stream
135223

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+
136233
// Dump body on error
137234
if (util.isStream(opts.body)) {
138235
opts.body?.on('error', () => {}).resume()
139236
}
140237

141238
// Check if the response is stale
142-
const now = Date.now()
143-
if (now >= value.staleAt) {
239+
if (needsRevalidation(now, value, age, requestCacheControl)) {
144240
if (now >= value.deleteAt) {
145241
// Safety check in case the store gave us a response that should've been
146242
// deleted already

0 commit comments

Comments
 (0)