Skip to content

Commit 992c6d8

Browse files
mcollinaclaudeUzlopak
authored
fix: implement proper stale-while-revalidate behavior per RFC 5861 (#4492)
* fix: implement proper stale-while-revalidate behavior per RFC 5861 The stale-while-revalidate cache directive was not working as intended. Instead of returning stale content immediately and revalidating in the background, it was performing synchronous revalidation, defeating the primary purpose of reducing latency. This fix: - Returns stale content immediately when within the stale-while-revalidate window - Performs background revalidation asynchronously to update the cache - Updates tests to reflect the correct RFC 5861 behavior Fixes #4471 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Matteo Collina <hello@matteocollina.com> * test: use timers/promises for sleep instead of creating promises Replace manual Promise creation with setTimeout from timers/promises for cleaner and more idiomatic async delays in tests. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Matteo Collina <hello@matteocollina.com> * fix: use process.nextTick instead of setImmediate for background revalidation setImmediate doesn't work properly with fake timers in tests, causing background revalidation to not trigger. process.nextTick works correctly with both real time and fake timers. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Matteo Collina <hello@matteocollina.com> * Update cache.js Co-authored-by: Aras Abbasi <aras.abbasi@googlemail.com> --------- Signed-off-by: Matteo Collina <hello@matteocollina.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Aras Abbasi <aras.abbasi@googlemail.com>
1 parent d79870d commit 992c6d8

2 files changed

Lines changed: 274 additions & 22 deletions

File tree

lib/interceptor/cache.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ function needsRevalidation (result, cacheControlDirectives) {
5656
return false
5757
}
5858

59+
/**
60+
* Check if we're within the stale-while-revalidate window for a stale response
61+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
62+
* @returns {boolean}
63+
*/
64+
function withinStaleWhileRevalidateWindow (result) {
65+
const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
66+
if (!staleWhileRevalidate) {
67+
return false
68+
}
69+
70+
const now = Date.now()
71+
const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
72+
return now <= staleWhileRevalidateExpiry
73+
}
74+
5975
/**
6076
* @param {DispatchFn} dispatch
6177
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
@@ -231,6 +247,51 @@ function handleResult (
231247
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
232248
}
233249

250+
// RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
251+
// and revalidate in background
252+
if (withinStaleWhileRevalidateWindow(result)) {
253+
// Serve stale response immediately
254+
sendCachedValue(handler, opts, result, age, null, true)
255+
256+
// Start background revalidation (fire-and-forget)
257+
queueMicrotask(() => {
258+
let headers = {
259+
...opts.headers,
260+
'if-modified-since': new Date(result.cachedAt).toUTCString()
261+
}
262+
263+
if (result.etag) {
264+
headers['if-none-match'] = result.etag
265+
}
266+
267+
if (result.vary) {
268+
headers = {
269+
...headers,
270+
...result.vary
271+
}
272+
}
273+
274+
// Background revalidation - update cache if we get new data
275+
dispatch(
276+
{
277+
...opts,
278+
headers
279+
},
280+
new CacheHandler(globalOpts, cacheKey, {
281+
// Silent handler that just updates the cache
282+
onRequestStart () {},
283+
onRequestUpgrade () {},
284+
onResponseStart () {},
285+
onResponseData () {},
286+
onResponseEnd () {},
287+
onResponseError () {}
288+
})
289+
)
290+
})
291+
292+
return true
293+
}
294+
234295
let withinStaleIfErrorThreshold = false
235296
const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
236297
if (staleIfErrorExpiry) {

0 commit comments

Comments
 (0)