Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ function withinStaleWhileRevalidateWindow (result) {
return now <= staleWhileRevalidateExpiry
}

/**
* Returns the if-modified-since value for revalidating a stored response.
* https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.3
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
* @returns {string}
*/
function getIfModifiedSinceValue (result) {
if (typeof result.headers['last-modified'] === 'string' && result.headers['last-modified'] !== '') {
return result.headers['last-modified']
}

if (typeof result.headers.date === 'string' && result.headers.date !== '') {
return result.headers.date
}

return new Date(result.cachedAt).toUTCString()
}

/**
* @param {DispatchFn} dispatch
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
Expand Down Expand Up @@ -307,7 +325,7 @@ function handleResult (
queueMicrotask(() => {
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
'if-modified-since': getIfModifiedSinceValue(result)
}

if (result.etag) {
Expand Down Expand Up @@ -351,7 +369,7 @@ function handleResult (

const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
'if-modified-since': getIfModifiedSinceValue(result)
}

if (result.etag) {
Expand Down
170 changes: 170 additions & 0 deletions test/interceptors/cache-revalidate-stale.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,173 @@ describe('revalidates the request, handles 304s during stale-while-revalidate',
test('using if-none-match', async () => await revalidateTest(true))
test('using if-modified-since', async () => await revalidateTest(false))
})

describe('if-modified-since value on revalidation requests (RFC 9110 §13.1.3)', () => {
const LAST_MODIFIED = 'Sat, 09 Oct 2010 14:28:02 GMT'

test('sends the stored last-modified value verbatim', async () => {
const clock = FakeTimers.install({
now: new Date('2028-03-15T12:00:00.000Z').getTime()
})
after(() => clock.uninstall())

const revalidationRequests = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=1')
res.setHeader('Last-Modified', LAST_MODIFIED)

if (req.headers['if-modified-since']) {
revalidationRequests.push(req.headers['if-modified-since'])
res.statusCode = 304
res.end()
} else {
res.end('hello world')
}
})

server.listen(0)
await once(server, 'listening')

const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())

after(async () => {
server.close()
await dispatcher.close()
})

const url = `http://localhost:${server.address().port}`

// initial request, cache the response
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')
}

// let the response go stale, forcing a revalidation request
clock.tick(1500)

{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')
}

strictEqual(revalidationRequests.length, 1)
strictEqual(revalidationRequests[0], LAST_MODIFIED)
})

test('sends the stored last-modified value verbatim during stale-while-revalidate', async () => {
const clock = FakeTimers.install({
now: new Date('2028-03-15T12:00:00.000Z').getTime()
})
after(() => clock.uninstall())

const revalidationRequests = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', new Date(clock.now).toUTCString())
res.setHeader('Cache-Control', 'public, max-age=1, stale-while-revalidate=3600')
res.setHeader('Last-Modified', LAST_MODIFIED)

if (req.headers['if-modified-since']) {
revalidationRequests.push(req.headers['if-modified-since'])
res.statusCode = 304
res.end()
} else {
res.end('hello world')
}
})

server.listen(0)
await once(server, 'listening')

const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())

after(async () => {
server.close()
await dispatcher.close()
})

const url = `http://localhost:${server.address().port}`

// initial request, cache the response
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')
}

// within the stale-while-revalidate window, revalidated in the background
clock.tick(2000)

{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')

// wait for the background revalidation to complete
await setTimeout(100)
clock.tick(10)
await setTimeout(100)
}

strictEqual(revalidationRequests.length, 1)
strictEqual(revalidationRequests[0], LAST_MODIFIED)
})

test('falls back to the stored date value when there is no last-modified', async () => {
const clock = FakeTimers.install({
now: new Date('2028-03-15T12:00:00.000Z').getTime()
})
after(() => clock.uninstall())

// date header trailing the local clock, as with a skewed origin clock
const dateHeader = new Date(clock.now - 5000).toUTCString()

const revalidationRequests = []
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.sendDate = false
res.setHeader('Date', dateHeader)
res.setHeader('Cache-Control', 'public, max-age=10')

if (req.headers['if-modified-since']) {
revalidationRequests.push(req.headers['if-modified-since'])
res.statusCode = 304
res.end()
} else {
res.end('hello world')
}
})

server.listen(0)
await once(server, 'listening')

const dispatcher = new Client(`http://localhost:${server.address().port}`)
.compose(interceptors.cache())

after(async () => {
server.close()
await dispatcher.close()
})

const url = `http://localhost:${server.address().port}`

// initial request, cache the response
{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')
}

// let the response go stale (staleAt is based off of the date header)
clock.tick(6000)

{
const res = await request(url, { dispatcher })
strictEqual(await res.body.text(), 'hello world')
}

strictEqual(revalidationRequests.length, 1)
strictEqual(revalidationRequests[0], dateHeader)
})
})
Loading