Skip to content

Commit 7555b68

Browse files
piehorinokai
andauthoredFeb 15, 2024
fix: don't set SWR cdn cache control on stale responses (#259)
* chore: make header helper function name consistent with others * fix: don't set SWR cdn cache control on stale responses * fix: use must-revalidate for cdn when nextjs returns stale * test: update test assertions --------- Co-authored-by: Rob Stanford <me@robstanford.com>
1 parent e33bd93 commit 7555b68

File tree

5 files changed

+30
-34
lines changed

5 files changed

+30
-34
lines changed
 

‎src/build/content/prerendered.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
7474

7575
await Promise.all(
7676
Object.entries(manifest.routes).map(async ([route, meta]): Promise<void> => {
77-
const lastModified = meta.initialRevalidateSeconds ? 1 : Date.now()
77+
const lastModified = meta.initialRevalidateSeconds ? Date.now() - 31536000000 : Date.now()
7878
const key = routeToFilePath(route)
7979
let value: IncrementalCacheValue
8080
switch (true) {
@@ -124,7 +124,7 @@ export const copyFetchContent = async (ctx: PluginContext): Promise<void> => {
124124

125125
await Promise.all(
126126
paths.map(async (key): Promise<void> => {
127-
const lastModified = 1
127+
const lastModified = Date.now() - 31536000000
128128
const path = join(ctx.publishDir, 'cache/fetch-cache', key)
129129
const value = await buildFetchCacheValue(path)
130130
await writeCacheEntry(key, value, lastModified, ctx)

‎src/run/handlers/server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
66
import { TagsManifest, getTagsManifest } from '../config.js'
77
import {
88
adjustDateHeader,
9-
handleNextCacheHeader,
9+
setCacheStatusHeader,
1010
setCacheControlHeaders,
1111
setCacheTagsHeaders,
1212
setVaryHeaders,
@@ -85,7 +85,7 @@ export default async (request: Request) => {
8585
setCacheControlHeaders(response.headers, request)
8686
setCacheTagsHeaders(response.headers, request, tagsManifest)
8787
setVaryHeaders(response.headers, request, nextConfig)
88-
handleNextCacheHeader(response.headers)
88+
setCacheStatusHeader(response.headers)
8989

9090
// Temporary workaround for an issue where sending a response with an empty
9191
// body causes an unhandled error. This doesn't catch everything, but redirects are the

‎src/run/headers.ts

+12-21
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,6 @@ export const adjustDateHeader = async ({
163163
return
164164
}
165165

166-
if (lastModified === 1) {
167-
// epoch 1 time seems to not work well with the CDN
168-
// it causes to produce "Thu, 01 Jan 1970 00:00:00 GMT" date
169-
// header correctly but CDN calculates then AGE to be 0
170-
// causing response to be cached for entire max-age period
171-
// since the first hit
172-
// so instead of using 1 we use 1 year ago
173-
// this is done here instead at build time as it's unclear
174-
// what exactly is edge case in CDN and setting 1 year ago
175-
// from request time seems to work consistently
176-
lastModified = Date.now() - 31536000000
177-
}
178-
179166
const lastModifiedDate = new Date(lastModified)
180167
// Show actual date of the function call in the date header
181168
headers.set('x-nextjs-date', headers.get('date') ?? lastModifiedDate.toUTCString())
@@ -195,16 +182,20 @@ export const setCacheControlHeaders = (headers: Headers, request: Request) => {
195182
!headers.has('cdn-cache-control') &&
196183
!headers.has('netlify-cdn-cache-control')
197184
) {
198-
const privateCacheControl = omitHeaderValues(cacheControl, [
185+
const browserCacheControl = omitHeaderValues(cacheControl, [
199186
's-maxage',
200187
'stale-while-revalidate',
201188
])
202-
const sharedCacheControl = mapHeaderValues(cacheControl, (value) =>
203-
value === 'stale-while-revalidate' ? 'stale-while-revalidate=31536000' : value,
204-
)
205-
206-
headers.set('cache-control', privateCacheControl || 'public, max-age=0, must-revalidate')
207-
headers.set('netlify-cdn-cache-control', sharedCacheControl)
189+
const cdnCacheControl =
190+
// if we are serving already stale response, instruct edge to not attempt to cache that response
191+
headers.get('x-nextjs-cache') === 'STALE'
192+
? 'public, max-age=0, must-revalidate'
193+
: mapHeaderValues(cacheControl, (value) =>
194+
value === 'stale-while-revalidate' ? 'stale-while-revalidate=31536000' : value,
195+
)
196+
197+
headers.set('cache-control', browserCacheControl || 'public, max-age=0, must-revalidate')
198+
headers.set('netlify-cdn-cache-control', cdnCacheControl)
208199
}
209200
}
210201

@@ -232,7 +223,7 @@ const NEXT_CACHE_TO_CACHE_STATUS: Record<string, string> = {
232223
* a Cache-Status header for Next cache so users inspect that together with CDN cache status
233224
* and not on its own.
234225
*/
235-
export const handleNextCacheHeader = (headers: Headers) => {
226+
export const setCacheStatusHeader = (headers: Headers) => {
236227
const nextCache = headers.get('x-nextjs-cache')
237228
if (typeof nextCache === 'string') {
238229
if (nextCache in NEXT_CACHE_TO_CACHE_STATUS) {

‎tests/integration/cache-handler.test.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('page router', () => {
6161
expect(call1.headers, 'a stale page served with swr').toEqual(
6262
expect.objectContaining({
6363
'cache-status': '"Next.js"; hit; fwd=stale',
64-
'netlify-cdn-cache-control': 's-maxage=5, stale-while-revalidate=31536000',
64+
'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate',
6565
}),
6666
)
6767

@@ -92,7 +92,8 @@ describe('page router', () => {
9292
'the rendered date in regenerated page is newer than initial stale page',
9393
).toBeGreaterThan(0)
9494
expect(
95-
call2.headers['date'].toString().localeCompare(call1.headers['date'].toString()),
95+
new Date(call2.headers['date'].toString()).getTime() -
96+
new Date(call1.headers['date'].toString()).getTime(),
9697
'the date header of regenerated page is newer than initial stale page',
9798
).toBeGreaterThan(0)
9899

@@ -171,7 +172,8 @@ describe('page router', () => {
171172
const call4Date = load(call4.body)('[data-testid="date-now"]').text()
172173
expect(call4Date, 'the date was not cached').not.toBe(call3Date)
173174
expect(
174-
call4.headers['date'].toString().localeCompare(call3.headers['date'].toString()),
175+
new Date(call4.headers['date'].toString()).getTime() -
176+
new Date(call3.headers['date'].toString()).getTime(),
175177
'the date header of regenerated page is newer than initial stale page',
176178
).toBeGreaterThan(0)
177179
expect(call4.statusCode).toBe(200)
@@ -217,7 +219,7 @@ describe('app router', () => {
217219
// It will be stale instead of hit
218220
expect.objectContaining({
219221
'cache-status': '"Next.js"; hit; fwd=stale',
220-
'netlify-cdn-cache-control': 's-maxage=5, stale-while-revalidate=31536000',
222+
'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate',
221223
}),
222224
)
223225
expect(
@@ -284,7 +286,8 @@ describe('app router', () => {
284286
expect(cached.statusCode).toBe(200)
285287
expect(cachedDate, 'the date is not stale').not.toBe(staleDate)
286288
expect(
287-
cached.headers['date'].toString().localeCompare(post1.headers['date'].toString()),
289+
new Date(cached.headers['date'].toString()).getTime() -
290+
new Date(post1.headers['date'].toString()).getTime(),
288291
'the date header of regenerated page is newer than initial stale page',
289292
).toBeGreaterThan(0)
290293
expect(cached.headers, 'a cache hit after dynamically regenerating the stale page').toEqual(
@@ -378,7 +381,8 @@ describe('route', () => {
378381
)
379382
expect(call2Time.localeCompare(call1Time), 'the rendered date is new').toBeGreaterThan(0)
380383
expect(
381-
call2.headers['date'].toString().localeCompare(call1.headers['date'].toString()),
384+
new Date(call2.headers['date'].toString()).getTime() -
385+
new Date(call1.headers['date'].toString()).getTime(),
382386
'the date header of regenerated route is newer than initial stale route',
383387
).toBeGreaterThan(0)
384388
expect(
@@ -429,7 +433,8 @@ describe('route', () => {
429433
)
430434
expect(call4Time.localeCompare(call3Time), 'the rendered date is new').toBeGreaterThan(0)
431435
expect(
432-
call4.headers['date'].toString().localeCompare(call3.headers['date'].toString()),
436+
new Date(call4.headers['date'].toString()).getTime() -
437+
new Date(call3.headers['date'].toString()).getTime(),
433438
'the date header of regenerated route is newer than previously stale route',
434439
).toBeGreaterThan(0)
435440
expect(

‎tests/integration/fetch-handler.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ test<FixtureTestContext>('if the fetch call is cached correctly (cached page res
213213
expect(post1.headers, 'a stale page served with swr').toEqual(
214214
expect.objectContaining({
215215
'cache-status': '"Next.js"; hit; fwd=stale',
216-
'netlify-cdn-cache-control': 's-maxage=5, stale-while-revalidate=31536000',
216+
'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate',
217217
}),
218218
)
219219

@@ -290,7 +290,7 @@ test<FixtureTestContext>('if the fetch call is cached correctly (cached page res
290290
expect(post3.headers, 'a stale page served with swr').toEqual(
291291
expect.objectContaining({
292292
'cache-status': '"Next.js"; hit; fwd=stale',
293-
'netlify-cdn-cache-control': 's-maxage=5, stale-while-revalidate=31536000',
293+
'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate',
294294
}),
295295
)
296296

0 commit comments

Comments
 (0)
Please sign in to comment.