Skip to content

Commit 5b477db

Browse files
piehorinokai
andauthored
feat: remove x-nextjs-cache header and add Cache-Status instead (#158)
* feat: remove x-nextjs-cache header and add Cache-Status instead * chore: rename cache name from 'Next Cache' to 'Next.js' * test: adjust test after removing x-nextjs-cache header and adding cache-status * test: cache-status can have multiple values, adjust assertions for that * test: e2e quirks worked out --------- Co-authored-by: Rob Stanford <[email protected]>
1 parent 1ab8d82 commit 5b477db

8 files changed

+85
-24
lines changed

src/run/handlers/server.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
55
import { TagsManifest, getTagsManifest } from '../config.js'
66
import {
77
adjustDateHeader,
8+
handleNextCacheHeader,
89
setCacheControlHeaders,
910
setCacheTagsHeaders,
1011
setVaryHeaders,
@@ -59,6 +60,7 @@ export default async (request: Request) => {
5960
setCacheControlHeaders(response.headers, request)
6061
setCacheTagsHeaders(response.headers, request, tagsManifest)
6162
setVaryHeaders(response.headers, request, nextConfig)
63+
handleNextCacheHeader(response.headers)
6264

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

src/run/headers.ts

+29
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,32 @@ export const setCacheTagsHeaders = (headers: Headers, request: Request, manifest
135135
const tags = manifest[path]
136136
headers.set('cache-tag', tags)
137137
}
138+
139+
/**
140+
* https://httpwg.org/specs/rfc9211.html
141+
*
142+
* We only should get HIT and MISS statuses from Next cache.
143+
* We will ignore other statuses and will not set Cache-Status header in those cases.
144+
*/
145+
const NEXT_CACHE_TO_CACHE_STATUS: Record<string, string> = {
146+
HIT: `hit`,
147+
MISS: `miss,`,
148+
}
149+
150+
/**
151+
* x-nextjs-cache header will be confusing to users as very often it will be MISS
152+
* even when the CDN serves cached the response. So we'll remove it and instead add
153+
* a Cache-Status header for Next cache so users inspect that together with CDN cache status
154+
* and not on its own.
155+
*/
156+
export const handleNextCacheHeader = (headers: Headers) => {
157+
const nextCache = headers.get('x-nextjs-cache')
158+
if (typeof nextCache === 'string') {
159+
if (nextCache in NEXT_CACHE_TO_CACHE_STATUS) {
160+
const cacheStatus = NEXT_CACHE_TO_CACHE_STATUS[nextCache]
161+
headers.set('cache-status', `"Next.js"; ${cacheStatus}`)
162+
}
163+
164+
headers.delete('x-nextjs-cache')
165+
}
166+
}

tests/e2e/page-router.test.ts

+32-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,28 @@ test.afterAll(async ({}, testInfo) => {
1212
})
1313

1414
test('Static revalidate works correctly', async ({ page }) => {
15+
// this disables browser cache - otherwise If-None-Match header
16+
// is added to repeat requests which result in actual 304 response
17+
// with custom headers from function not being returned
18+
// additionally Playwright wrongly report 304 as 200
19+
// https://github.com/microsoft/playwright/issues/27573
20+
// which makes the assertions confusing
21+
// see https://playwright.dev/docs/api/class-browsercontext#browser-context-route
22+
// > Enabling routing disables http cache.
23+
// and https://stackoverflow.com/questions/68522170/playwright-disable-caching-of-webpage-so-i-can-fetch-new-elements-after-scrollin
24+
// note - this is likely the same problem that cause assertions at the bottom to be commented out
25+
// generally we shouldn't do that
26+
page.route('**', (route) => route.continue())
27+
1528
const response1 = await page.goto(new URL('static/revalidate-manual', ctx.url).href)
1629
const headers1 = response1?.headers() || {}
1730
expect(response1?.status()).toBe(200)
18-
expect(headers1['x-nextjs-cache']).toBe('HIT')
31+
expect(headers1['x-nextjs-cache']).toBeUndefined()
32+
// first time hitting this route - we will invoke function and see
33+
// Next cache hit status in the response because it was prerendered
34+
// at build time
35+
expect(headers1['cache-status']).toMatch(/"Netlify Edge"; fwd=miss/m)
36+
expect(headers1['cache-status']).toMatch(/"Next.js"; hit/m)
1937

2038
const date1 = await page.textContent('[data-testid="date-now"]')
2139
const h1 = await page.textContent('h1')
@@ -24,7 +42,11 @@ test('Static revalidate works correctly', async ({ page }) => {
2442
const response2 = await page.goto(new URL('static/revalidate-manual', ctx.url).href)
2543
const headers2 = response2?.headers() || {}
2644
expect(response2?.status()).toBe(200)
27-
expect(headers2['x-nextjs-cache']).toBe('HIT')
45+
expect(headers2['x-nextjs-cache']).toBeUndefined()
46+
// On CDN hit, Next cache status is not added to response anymore
47+
// (any cache-status set by functions is not added - this is a platform behavior
48+
// not runtime behavior)
49+
expect(headers2['cache-status']).toMatch(/"Netlify Edge"; hit/m)
2850

2951
// the page is cached
3052
const date2 = await page.textContent('[data-testid="date-now"]')
@@ -40,7 +62,14 @@ test('Static revalidate works correctly', async ({ page }) => {
4062
const response3 = await page.goto(new URL('static/revalidate-manual', ctx.url).href)
4163
const headers3 = response3?.headers() || {}
4264
expect(response3?.status()).toBe(200)
43-
expect(headers3['x-nextjs-cache']).toBe('HIT')
65+
expect(headers3?.['x-nextjs-cache']).toBeUndefined()
66+
// revalidate refreshes Next cache, but not CDN cache
67+
// so our request after revalidation means that Next cache is already
68+
// warmed up with fresh response, but CDN cache just knows that previously
69+
// cached response is stale, so we are hitting our function that serve
70+
// already cached response
71+
expect(headers3['cache-status']).toMatch(/"Next.js"; hit/m)
72+
expect(headers3['cache-status']).toMatch(/"Netlify Edge"; fwd=stale/m)
4473

4574
// the page has now an updated date
4675
// TODO: Cache purge is currently not working as expected

tests/integration/cache-handler.test.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('page router', () => {
5555
expect(load(call1.body)('h1').text()).toBe('Show #71')
5656
expect(call1.headers, 'a cache hit on the first invocation of a prerendered page').toEqual(
5757
expect.objectContaining({
58-
'x-nextjs-cache': 'HIT',
58+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
5959
'netlify-cdn-cache-control': 's-maxage=5, stale-while-revalidate=31536000',
6060
}),
6161
)
@@ -72,7 +72,7 @@ describe('page router', () => {
7272
expect(call2.statusCode).toBe(200)
7373
expect(call2.headers, 'a cache miss on a stale page').toEqual(
7474
expect.objectContaining({
75-
'x-nextjs-cache': 'MISS',
75+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
7676
}),
7777
)
7878
expect(call2Date, 'the date was cached and is matching the initial one').not.toBe(call1Date)
@@ -84,7 +84,7 @@ describe('page router', () => {
8484

8585
expect(callLater2.headers, 'date header matches the cached value').toEqual(
8686
expect.objectContaining({
87-
'x-nextjs-cache': 'HIT',
87+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
8888
date: callLater.headers['date'],
8989
}),
9090
)
@@ -99,7 +99,7 @@ describe('page router', () => {
9999
expect(call3.statusCode).toBe(200)
100100
expect(call3.headers, 'a cache hit after dynamically regenerating the stale page').toEqual(
101101
expect.objectContaining({
102-
'x-nextjs-cache': 'HIT',
102+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
103103
}),
104104
)
105105
})
@@ -131,7 +131,7 @@ describe('app router', () => {
131131
expect(load(post1.body)('h1').text()).toBe('Revalidate Fetch')
132132
expect(post1.headers, 'a cache hit on the first invocation of a prerendered page').toEqual(
133133
expect.objectContaining({
134-
'x-nextjs-cache': 'HIT',
134+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
135135
'netlify-cdn-cache-control': 's-maxage=5, stale-while-revalidate=31536000',
136136
}),
137137
)
@@ -143,7 +143,7 @@ describe('app router', () => {
143143
expect(load(post3.body)('h1').text()).toBe('Revalidate Fetch')
144144
expect(post3.headers, 'a cache miss on a non prerendered page').toEqual(
145145
expect.objectContaining({
146-
'x-nextjs-cache': 'MISS',
146+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
147147
}),
148148
)
149149

@@ -157,7 +157,7 @@ describe('app router', () => {
157157
expect(stale.statusCode).toBe(200)
158158
expect(stale.headers, 'a cache miss on a stale page').toEqual(
159159
expect.objectContaining({
160-
'x-nextjs-cache': 'MISS',
160+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
161161
}),
162162
)
163163
// it should have a new date rendered
@@ -173,7 +173,7 @@ describe('app router', () => {
173173
expect(staleDate, 'the date was not cached').toBe(cachedDate)
174174
expect(cached.headers, 'a cache hit after dynamically regenerating the stale page').toEqual(
175175
expect.objectContaining({
176-
'x-nextjs-cache': 'HIT',
176+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
177177
}),
178178
)
179179
})
@@ -228,7 +228,7 @@ describe('route', () => {
228228
})
229229
expect(call1.headers, 'a cache hit on the first invocation of a prerendered route').toEqual(
230230
expect.objectContaining({
231-
'x-nextjs-cache': 'HIT',
231+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
232232
}),
233233
)
234234
// wait to have a stale route
@@ -242,7 +242,7 @@ describe('route', () => {
242242
expect(call2Body).toMatchObject({ data: expect.objectContaining({ id: 1 }) })
243243
expect(call2.headers, 'a cache miss on a stale route').toEqual(
244244
expect.objectContaining({
245-
'x-nextjs-cache': 'MISS',
245+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
246246
}),
247247
)
248248
expect(call1Time, 'the date is a new one on a stale route').not.toBe(call2Time)
@@ -257,7 +257,7 @@ describe('route', () => {
257257
expect(call3Body).toMatchObject({ data: expect.objectContaining({ id: 1 }) })
258258
expect(call3.headers, 'a cache hit after dynamically regenerating the stale route').toEqual(
259259
expect.objectContaining({
260-
'x-nextjs-cache': 'HIT',
260+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
261261
}),
262262
)
263263
expect(call3Time, 'the date was cached as well').toBe(call2Time)

tests/integration/fetch-handler.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ test<FixtureTestContext>('if the fetch call is cached correctly', async (ctx) =>
111111
expect(post1Name).toBe('Under the Dome')
112112
expect(post1.headers, 'the page should be a miss').toEqual(
113113
expect.objectContaining({
114-
'x-nextjs-cache': 'MISS',
114+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
115115
}),
116116
)
117117

tests/integration/page-router.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ test<FixtureTestContext>('Should revalidate path with On-demand Revalidation', a
7070
const dateCacheInitial = load(staticPageInitial.body)('[data-testid="date-now"]').text()
7171

7272
expect(staticPageInitial.statusCode).toBe(200)
73-
expect(staticPageInitial.headers?.['x-nextjs-cache']).toBe('HIT')
73+
expect(staticPageInitial.headers?.['cache-status']).toMatch(/"Next.js"; hit/)
7474
const blobDataInitial = await ctx.blobStore.get(encodeBlobKey('static/revalidate-manual'), {
7575
type: 'json',
7676
})
@@ -91,7 +91,7 @@ test<FixtureTestContext>('Should revalidate path with On-demand Revalidation', a
9191
expect(blobDateInitial).not.toBe(blobDateRevalidated)
9292

9393
const staticPageRevalidated = await invokeFunction(ctx, { url: '/static/revalidate-manual' })
94-
expect(staticPageRevalidated.headers?.['x-nextjs-cache']).toBe('HIT')
94+
expect(staticPageRevalidated.headers?.['cache-status']).toMatch(/"Next.js"; hit/)
9595
const dateCacheRevalidated = load(staticPageRevalidated.body)('[data-testid="date-now"]').text()
9696

9797
console.log({ dateCacheInitial, dateCacheRevalidated })

tests/integration/revalidate-path.test.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,16 @@ test<FixtureTestContext>('should revalidate a route by path', async (ctx) => {
4747
expect(post1.statusCode).toBe(200)
4848
expect(post1Route2.statusCode).toBe(200)
4949
expect(load(post1.body)('h1').text()).toBe('Hello, Statically fetched show 1')
50+
5051
expect(post1.headers, 'a cache hit on the first invocation of a prerendered page').toEqual(
5152
expect.objectContaining({
52-
'x-nextjs-cache': 'HIT',
53+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
5354
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
5455
}),
5556
)
5657
expect(post1Route2.headers, 'a cache hit on the first invocation of a prerendered page').toEqual(
5758
expect.objectContaining({
58-
'x-nextjs-cache': 'HIT',
59+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
5960
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
6061
}),
6162
)
@@ -80,13 +81,13 @@ test<FixtureTestContext>('should revalidate a route by path', async (ctx) => {
8081
expect(load(post2.body)('h1').text()).toBe('Hello, Statically fetched show 1')
8182
expect(post2.headers, 'a cache miss on the on demand revalidated path /1').toEqual(
8283
expect.objectContaining({
83-
'x-nextjs-cache': 'MISS',
84+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
8485
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
8586
}),
8687
)
8788
expect(post2Route2.headers, 'a cache miss on the on demand revalidated path /2').toEqual(
8889
expect.objectContaining({
89-
'x-nextjs-cache': 'MISS',
90+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
9091
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
9192
}),
9293
)

tests/integration/revalidate-tags.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
3939
expect(load(post1.body)('h1').text()).toBe('Hello, Static Fetch 1')
4040
expect(post1.headers, 'a cache hit on the first invocation of a prerendered page').toEqual(
4141
expect.objectContaining({
42-
'x-nextjs-cache': 'HIT',
42+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
4343
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
4444
}),
4545
)
@@ -57,7 +57,7 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
5757
expect(load(post2.body)('h1').text()).toBe('Hello, Static Fetch 1')
5858
expect(post2.headers, 'a cache miss on the on demand revalidated page').toEqual(
5959
expect.objectContaining({
60-
'x-nextjs-cache': 'MISS',
60+
'cache-status': expect.stringMatching(/"Next.js"; miss/),
6161
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
6262
}),
6363
)
@@ -72,7 +72,7 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
7272
expect(load(post3.body)('h1').text()).toBe('Hello, Static Fetch 1')
7373
expect(post3.headers, 'a cache hit on the revalidated and regenerated page').toEqual(
7474
expect.objectContaining({
75-
'x-nextjs-cache': 'HIT',
75+
'cache-status': expect.stringMatching(/"Next.js"; hit/),
7676
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
7777
}),
7878
)

0 commit comments

Comments
 (0)