Skip to content

Commit

Permalink
Add ability to customize Cache-Control (#69802)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ijjk committed Sep 9, 2024
1 parent 6fa8982 commit 24647b9
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 7 deletions.
17 changes: 14 additions & 3 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2587,6 +2587,13 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
)

if (isPreviewMode) {
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}

if (!cacheEntry) {
if (ssgCacheKey && !(isOnDemandRevalidate && revalidateOnlyGenerated)) {
// A cache entry might not be generated if a response is written
Expand Down Expand Up @@ -2719,7 +2726,9 @@ export default abstract class Server<ServerOptions extends Options = Options> {
// for the revalidate value
addRequestMeta(req, 'notFoundRevalidate', cacheEntry.revalidate)

if (cacheEntry.revalidate) {
// If cache control is already set on the response we don't
// override it to allow users to customize it via next.config
if (cacheEntry.revalidate && !res.getHeader('Cache-Control')) {
res.setHeader(
'Cache-Control',
formatRevalidate({
Expand All @@ -2740,7 +2749,9 @@ export default abstract class Server<ServerOptions extends Options = Options> {
await this.render404(req, res, { pathname, query }, false)
return null
} else if (cachedData.kind === 'REDIRECT') {
if (cacheEntry.revalidate) {
// If cache control is already set on the response we don't
// override it to allow users to customize it via next.config
if (cacheEntry.revalidate && !res.getHeader('Cache-Control')) {
res.setHeader(
'Cache-Control',
formatRevalidate({
Expand Down Expand Up @@ -3223,7 +3234,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
if (setHeaders) {
res.setHeader(
'Cache-Control',
'no-cache, no-store, max-age=0, must-revalidate'
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ export async function initialize(opts: {
// 404 case
res.setHeader(
'Cache-Control',
'no-cache, no-store, max-age=0, must-revalidate'
'private, no-cache, no-store, max-age=0, must-revalidate'
)

// 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.
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/server/send-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export async function sendRenderResult({
res.setHeader('X-Powered-By', 'Next.js')
}

if (typeof revalidate !== 'undefined') {
// If cache control is already set on the response we don't
// override it to allow users to customize it via next.config
if (typeof revalidate !== 'undefined' && !res.getHeader('Cache-Control')) {
res.setHeader(
'Cache-Control',
formatRevalidate({
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/app-dir/custom-cache-control/app/app-ssg/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const revalidate = 120

export function generateStaticParams() {
return [
{
slug: 'first',
},
]
}

export default function Page({ params }) {
return (
<>
<p>/app-ssg/[slug]</p>
<p>{JSON.stringify(params)}</p>
</>
)
}
9 changes: 9 additions & 0 deletions test/e2e/app-dir/custom-cache-control/app/app-ssr/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const dynamic = 'force-dynamic'

export default function Page() {
return (
<>
<p>/app-ssr</p>
</>
)
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/custom-cache-control/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
84 changes: 84 additions & 0 deletions test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { nextTestSetup } from 'e2e-utils'

describe('custom-cache-control', () => {
const { next, isNextDev, isNextDeploy } = nextTestSetup({
files: __dirname,
})

if (isNextDeploy) {
// customizing these headers won't apply on environments
// where headers are applied outside of the Next.js server
it('should skip for deploy', () => {})
return
}

it('should have custom cache-control for app-ssg prerendered', async () => {
const res = await next.fetch('/app-ssg/first')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=30'
)
})

it('should have custom cache-control for app-ssg lazy', async () => {
const res = await next.fetch('/app-ssg/lazy')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=31'
)
})
;(process.env.__NEXT_EXPERIMENTAL_PPR ? it.skip : it)(
'should have default cache-control for app-ssg another',
async () => {
const res = await next.fetch('/app-ssg/another')
// eslint-disable-next-line jest/no-standalone-expect
expect(res.headers.get('cache-control')).toBe(
isNextDev
? 'no-store, must-revalidate'
: 's-maxage=120, stale-while-revalidate'
)
}
)

it('should have custom cache-control for app-ssr', async () => {
const res = await next.fetch('/app-ssr')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=32'
)
})

it('should have custom cache-control for auto static page', async () => {
const res = await next.fetch('/pages-auto-static')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=33'
)
})

it('should have custom cache-control for pages-ssg prerendered', async () => {
const res = await next.fetch('/pages-ssg/first')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=34'
)
})

it('should have custom cache-control for pages-ssg lazy', async () => {
const res = await next.fetch('/pages-ssg/lazy')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=35'
)
})

it('should have default cache-control for pages-ssg another', async () => {
const res = await next.fetch('/pages-ssg/another')
expect(res.headers.get('cache-control')).toBe(
isNextDev
? 'no-store, must-revalidate'
: 's-maxage=120, stale-while-revalidate'
)
})

it('should have default cache-control for pages-ssr', async () => {
const res = await next.fetch('/pages-ssr')
expect(res.headers.get('cache-control')).toBe(
isNextDev ? 'no-store, must-revalidate' : 's-maxage=36'
)
})
})
74 changes: 74 additions & 0 deletions test/e2e/app-dir/custom-cache-control/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
headers() {
return [
{
source: '/app-ssg/first',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=30',
},
],
},
{
source: '/app-ssg/lazy',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=31',
},
],
},
{
source: '/app-ssr',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=32',
},
],
},
{
source: '/pages-auto-static',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=33',
},
],
},
{
source: '/pages-ssg/first',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=34',
},
],
},
{
source: '/pages-ssg/lazy',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=35',
},
],
},
{
source: '/pages-ssr',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=36',
},
],
},
]
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return (
<>
<p>/pages-auto-static</p>
</>
)
}
29 changes: 29 additions & 0 deletions test/e2e/app-dir/custom-cache-control/pages/pages-ssg/[slug].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function getStaticProps({ params }) {
return {
props: {
now: Date.now(),
params,
},
revalidate: 120,
}
}

export function getStaticPaths() {
return {
paths: [
{
params: { slug: 'first' },
},
],
fallback: 'blocking',
}
}

export default function Page({ params }) {
return (
<>
<p>/pages-ssg/[slug]</p>
<p>{JSON.stringify(params)}</p>
</>
)
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/custom-cache-control/pages/pages-ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function getServerSideProps() {
return {
props: {
now: Date.now(),
},
}
}

export default function Page() {
return (
<>
<p>/pages-ssr</p>
</>
)
}
4 changes: 3 additions & 1 deletion test/integration/404-page/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ describe('404 Page Support', () => {
await killApp(app)

expect(cache404).toBe(null)
expect(cacheNext).toBe('no-cache, no-store, max-age=0, must-revalidate')
expect(cacheNext).toBe(
'private, no-cache, no-store, max-age=0, must-revalidate'
)
})

it('shows error with getInitialProps in pages/404 build', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/production/pages-dir/production/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ createNextDescribe(

expect(res.status).toBe(404)
expect(res.headers.get('Cache-Control')).toBe(
'no-cache, no-store, max-age=0, must-revalidate'
'private, no-cache, no-store, max-age=0, must-revalidate'
)
})

Expand Down

0 comments on commit 24647b9

Please sign in to comment.