Skip to content

Commit a916dfc

Browse files
authored
Add ability to customize Cache-Control (#69802)
This continues #39707 bringing the changes up to date with canary and adds test cases to ensure it's working as expected. Closes: #22319 Closes: #39707 Closes: NDX-148
1 parent 1dc3f39 commit a916dfc

File tree

13 files changed

+266
-7
lines changed

13 files changed

+266
-7
lines changed

packages/next/src/server/base-server.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2995,6 +2995,13 @@ export default abstract class Server<
29952995
}
29962996
)
29972997

2998+
if (isPreviewMode) {
2999+
res.setHeader(
3000+
'Cache-Control',
3001+
'private, no-cache, no-store, max-age=0, must-revalidate'
3002+
)
3003+
}
3004+
29983005
if (!cacheEntry) {
29993006
if (ssgCacheKey && !(isOnDemandRevalidate && revalidateOnlyGenerated)) {
30003007
// A cache entry might not be generated if a response is written
@@ -3181,7 +3188,9 @@ export default abstract class Server<
31813188
// for the revalidate value
31823189
addRequestMeta(req, 'notFoundRevalidate', cacheEntry.revalidate)
31833190

3184-
if (cacheEntry.revalidate) {
3191+
// If cache control is already set on the response we don't
3192+
// override it to allow users to customize it via next.config
3193+
if (cacheEntry.revalidate && !res.getHeader('Cache-Control')) {
31853194
res.setHeader(
31863195
'Cache-Control',
31873196
formatRevalidate({
@@ -3202,7 +3211,9 @@ export default abstract class Server<
32023211
await this.render404(req, res, { pathname, query }, false)
32033212
return null
32043213
} else if (cachedData.kind === CachedRouteKind.REDIRECT) {
3205-
if (cacheEntry.revalidate) {
3214+
// If cache control is already set on the response we don't
3215+
// override it to allow users to customize it via next.config
3216+
if (cacheEntry.revalidate && !res.getHeader('Cache-Control')) {
32063217
res.setHeader(
32073218
'Cache-Control',
32083219
formatRevalidate({
@@ -3693,7 +3704,7 @@ export default abstract class Server<
36933704
if (setHeaders) {
36943705
res.setHeader(
36953706
'Cache-Control',
3696-
'no-cache, no-store, max-age=0, must-revalidate'
3707+
'private, no-cache, no-store, max-age=0, must-revalidate'
36973708
)
36983709
}
36993710

packages/next/src/server/lib/router-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ export async function initialize(opts: {
540540
// 404 case
541541
res.setHeader(
542542
'Cache-Control',
543-
'no-cache, no-store, max-age=0, must-revalidate'
543+
'private, no-cache, no-store, max-age=0, must-revalidate'
544544
)
545545

546546
// Short-circuit favicon.ico serving so that the 404 page doesn't get built as favicon is requested by the browser when loading any route.

packages/next/src/server/send-payload.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export async function sendRenderResult({
5959
res.setHeader('X-Powered-By', 'Next.js')
6060
}
6161

62-
if (typeof revalidate !== 'undefined') {
62+
// If cache control is already set on the response we don't
63+
// override it to allow users to customize it via next.config
64+
if (typeof revalidate !== 'undefined' && !res.getHeader('Cache-Control')) {
6365
res.setHeader(
6466
'Cache-Control',
6567
formatRevalidate({
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const revalidate = 120
2+
3+
export function generateStaticParams() {
4+
return [
5+
{
6+
slug: 'first',
7+
},
8+
]
9+
}
10+
11+
export default function Page({ params }) {
12+
return (
13+
<>
14+
<p>/app-ssg/[slug]</p>
15+
<p>{JSON.stringify(params)}</p>
16+
</>
17+
)
18+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const dynamic = 'force-dynamic'
2+
3+
export default function Page() {
4+
return (
5+
<>
6+
<p>/app-ssr</p>
7+
</>
8+
)
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('custom-cache-control', () => {
4+
const { next, isNextDev, isNextDeploy } = nextTestSetup({
5+
files: __dirname,
6+
})
7+
8+
if (isNextDeploy) {
9+
// customizing these headers won't apply on environments
10+
// where headers are applied outside of the Next.js server
11+
it('should skip for deploy', () => {})
12+
return
13+
}
14+
15+
it('should have custom cache-control for app-ssg prerendered', async () => {
16+
const res = await next.fetch('/app-ssg/first')
17+
expect(res.headers.get('cache-control')).toBe(
18+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=30'
19+
)
20+
})
21+
22+
it('should have custom cache-control for app-ssg lazy', async () => {
23+
const res = await next.fetch('/app-ssg/lazy')
24+
expect(res.headers.get('cache-control')).toBe(
25+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=31'
26+
)
27+
})
28+
;(process.env.__NEXT_EXPERIMENTAL_PPR ? it.skip : it)(
29+
'should have default cache-control for app-ssg another',
30+
async () => {
31+
const res = await next.fetch('/app-ssg/another')
32+
// eslint-disable-next-line jest/no-standalone-expect
33+
expect(res.headers.get('cache-control')).toBe(
34+
isNextDev
35+
? 'no-store, must-revalidate'
36+
: 's-maxage=120, stale-while-revalidate'
37+
)
38+
}
39+
)
40+
41+
it('should have custom cache-control for app-ssr', async () => {
42+
const res = await next.fetch('/app-ssr')
43+
expect(res.headers.get('cache-control')).toBe(
44+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=32'
45+
)
46+
})
47+
48+
it('should have custom cache-control for auto static page', async () => {
49+
const res = await next.fetch('/pages-auto-static')
50+
expect(res.headers.get('cache-control')).toBe(
51+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=33'
52+
)
53+
})
54+
55+
it('should have custom cache-control for pages-ssg prerendered', async () => {
56+
const res = await next.fetch('/pages-ssg/first')
57+
expect(res.headers.get('cache-control')).toBe(
58+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=34'
59+
)
60+
})
61+
62+
it('should have custom cache-control for pages-ssg lazy', async () => {
63+
const res = await next.fetch('/pages-ssg/lazy')
64+
expect(res.headers.get('cache-control')).toBe(
65+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=35'
66+
)
67+
})
68+
69+
it('should have default cache-control for pages-ssg another', async () => {
70+
const res = await next.fetch('/pages-ssg/another')
71+
expect(res.headers.get('cache-control')).toBe(
72+
isNextDev
73+
? 'no-store, must-revalidate'
74+
: 's-maxage=120, stale-while-revalidate'
75+
)
76+
})
77+
78+
it('should have default cache-control for pages-ssr', async () => {
79+
const res = await next.fetch('/pages-ssr')
80+
expect(res.headers.get('cache-control')).toBe(
81+
isNextDev ? 'no-store, must-revalidate' : 's-maxage=36'
82+
)
83+
})
84+
})
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
headers() {
6+
return [
7+
{
8+
source: '/app-ssg/first',
9+
headers: [
10+
{
11+
key: 'Cache-Control',
12+
value: 's-maxage=30',
13+
},
14+
],
15+
},
16+
{
17+
source: '/app-ssg/lazy',
18+
headers: [
19+
{
20+
key: 'Cache-Control',
21+
value: 's-maxage=31',
22+
},
23+
],
24+
},
25+
{
26+
source: '/app-ssr',
27+
headers: [
28+
{
29+
key: 'Cache-Control',
30+
value: 's-maxage=32',
31+
},
32+
],
33+
},
34+
{
35+
source: '/pages-auto-static',
36+
headers: [
37+
{
38+
key: 'Cache-Control',
39+
value: 's-maxage=33',
40+
},
41+
],
42+
},
43+
{
44+
source: '/pages-ssg/first',
45+
headers: [
46+
{
47+
key: 'Cache-Control',
48+
value: 's-maxage=34',
49+
},
50+
],
51+
},
52+
{
53+
source: '/pages-ssg/lazy',
54+
headers: [
55+
{
56+
key: 'Cache-Control',
57+
value: 's-maxage=35',
58+
},
59+
],
60+
},
61+
{
62+
source: '/pages-ssr',
63+
headers: [
64+
{
65+
key: 'Cache-Control',
66+
value: 's-maxage=36',
67+
},
68+
],
69+
},
70+
]
71+
},
72+
}
73+
74+
module.exports = nextConfig
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Page() {
2+
return (
3+
<>
4+
<p>/pages-auto-static</p>
5+
</>
6+
)
7+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function getStaticProps({ params }) {
2+
return {
3+
props: {
4+
now: Date.now(),
5+
params,
6+
},
7+
revalidate: 120,
8+
}
9+
}
10+
11+
export function getStaticPaths() {
12+
return {
13+
paths: [
14+
{
15+
params: { slug: 'first' },
16+
},
17+
],
18+
fallback: 'blocking',
19+
}
20+
}
21+
22+
export default function Page({ params }) {
23+
return (
24+
<>
25+
<p>/pages-ssg/[slug]</p>
26+
<p>{JSON.stringify(params)}</p>
27+
</>
28+
)
29+
}

0 commit comments

Comments
 (0)