From 226a1e12afc54ebf8d385b486fb290edbb8fb514 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 6 Sep 2024 14:07:21 -0700 Subject: [PATCH 1/5] Add ability to customize Cache-Control --- packages/next/src/server/base-server.ts | 8 +- packages/next/src/server/send-payload.ts | 4 +- .../app/app-ssg/[slug]/page.tsx | 18 +++++ .../custom-cache-control/app/app-ssr/page.tsx | 9 +++ .../custom-cache-control/app/layout.tsx | 8 ++ .../custom-cache-control.test.ts | 74 +++++++++++++++++++ .../custom-cache-control/next.config.js | 74 +++++++++++++++++++ .../pages/pages-auto-static.tsx | 7 ++ .../pages/pages-ssg/[slug].tsx | 29 ++++++++ .../custom-cache-control/pages/pages-ssr.tsx | 15 ++++ 10 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 test/e2e/app-dir/custom-cache-control/app/app-ssg/[slug]/page.tsx create mode 100644 test/e2e/app-dir/custom-cache-control/app/app-ssr/page.tsx create mode 100644 test/e2e/app-dir/custom-cache-control/app/layout.tsx create mode 100644 test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts create mode 100644 test/e2e/app-dir/custom-cache-control/next.config.js create mode 100644 test/e2e/app-dir/custom-cache-control/pages/pages-auto-static.tsx create mode 100644 test/e2e/app-dir/custom-cache-control/pages/pages-ssg/[slug].tsx create mode 100644 test/e2e/app-dir/custom-cache-control/pages/pages-ssr.tsx diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 8ea811fbe0d2a..858161e2c180c 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -3181,7 +3181,9 @@ export default abstract class Server< // 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({ @@ -3202,7 +3204,9 @@ export default abstract class Server< await this.render404(req, res, { pathname, query }, false) return null } else if (cachedData.kind === CachedRouteKind.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({ diff --git a/packages/next/src/server/send-payload.ts b/packages/next/src/server/send-payload.ts index fb9aef5e82308..6299aee4abe47 100644 --- a/packages/next/src/server/send-payload.ts +++ b/packages/next/src/server/send-payload.ts @@ -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({ diff --git a/test/e2e/app-dir/custom-cache-control/app/app-ssg/[slug]/page.tsx b/test/e2e/app-dir/custom-cache-control/app/app-ssg/[slug]/page.tsx new file mode 100644 index 0000000000000..70eb489bb26b9 --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/app/app-ssg/[slug]/page.tsx @@ -0,0 +1,18 @@ +export const revalidate = 120 + +export function generateStaticParams() { + return [ + { + slug: 'first', + }, + ] +} + +export default function Page({ params }) { + return ( + <> +

/app-ssg/[slug]

+

{JSON.stringify(params)}

+ + ) +} diff --git a/test/e2e/app-dir/custom-cache-control/app/app-ssr/page.tsx b/test/e2e/app-dir/custom-cache-control/app/app-ssr/page.tsx new file mode 100644 index 0000000000000..69d3c4b3c18fe --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/app/app-ssr/page.tsx @@ -0,0 +1,9 @@ +export const dynamic = 'force-dynamic' + +export default function Page() { + return ( + <> +

/app-ssr

+ + ) +} diff --git a/test/e2e/app-dir/custom-cache-control/app/layout.tsx b/test/e2e/app-dir/custom-cache-control/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts new file mode 100644 index 0000000000000..c8b9f6973c51b --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts @@ -0,0 +1,74 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('custom-cache-control', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + + 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' + ) + }) + + it('should have default cache-control for app-ssg another', async () => { + const res = await next.fetch('/app-ssg/another') + 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' + ) + }) +}) diff --git a/test/e2e/app-dir/custom-cache-control/next.config.js b/test/e2e/app-dir/custom-cache-control/next.config.js new file mode 100644 index 0000000000000..7a61bbe7e6723 --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/next.config.js @@ -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 diff --git a/test/e2e/app-dir/custom-cache-control/pages/pages-auto-static.tsx b/test/e2e/app-dir/custom-cache-control/pages/pages-auto-static.tsx new file mode 100644 index 0000000000000..97f19eff92600 --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/pages/pages-auto-static.tsx @@ -0,0 +1,7 @@ +export default function Page() { + return ( + <> +

/pages-auto-static

+ + ) +} diff --git a/test/e2e/app-dir/custom-cache-control/pages/pages-ssg/[slug].tsx b/test/e2e/app-dir/custom-cache-control/pages/pages-ssg/[slug].tsx new file mode 100644 index 0000000000000..9b9c4d0182f75 --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/pages/pages-ssg/[slug].tsx @@ -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 ( + <> +

/pages-ssg/[slug]

+

{JSON.stringify(params)}

+ + ) +} diff --git a/test/e2e/app-dir/custom-cache-control/pages/pages-ssr.tsx b/test/e2e/app-dir/custom-cache-control/pages/pages-ssr.tsx new file mode 100644 index 0000000000000..5792abd592c3e --- /dev/null +++ b/test/e2e/app-dir/custom-cache-control/pages/pages-ssr.tsx @@ -0,0 +1,15 @@ +export function getServerSideProps() { + return { + props: { + now: Date.now(), + }, + } +} + +export default function Page() { + return ( + <> +

/pages-ssr

+ + ) +} From f4845fda42401151868b842849299c294ef081df Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 6 Sep 2024 14:37:39 -0700 Subject: [PATCH 2/5] update test cases --- packages/next/src/server/base-server.ts | 9 ++++++++- packages/next/src/server/lib/router-server.ts | 2 +- test/integration/404-page/test/index.test.js | 4 +++- test/production/pages-dir/production/test/index.test.ts | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 858161e2c180c..1dba9ac4ca6f8 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2995,6 +2995,13 @@ export default abstract class Server< } ) + 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 @@ -3697,7 +3704,7 @@ export default abstract class Server< 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' ) } diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index ed18f2378a71c..999a4d0805dd2 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -540,7 +540,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. diff --git a/test/integration/404-page/test/index.test.js b/test/integration/404-page/test/index.test.js index ca76076108567..5a59b227ee1eb 100644 --- a/test/integration/404-page/test/index.test.js +++ b/test/integration/404-page/test/index.test.js @@ -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 () => { diff --git a/test/production/pages-dir/production/test/index.test.ts b/test/production/pages-dir/production/test/index.test.ts index 82fd919e85e7c..8d16e87cdbe45 100644 --- a/test/production/pages-dir/production/test/index.test.ts +++ b/test/production/pages-dir/production/test/index.test.ts @@ -628,7 +628,7 @@ describe('Production Usage', () => { 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' ) }) From a062dbc623323c672a14f38aab295795eaf8262d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 6 Sep 2024 14:51:09 -0700 Subject: [PATCH 3/5] update ppr case --- .../custom-cache-control.test.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts index c8b9f6973c51b..158c953df1681 100644 --- a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts +++ b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts @@ -18,15 +18,18 @@ describe('custom-cache-control', () => { isNextDev ? 'no-store, must-revalidate' : 's-maxage=31' ) }) - - it('should have default cache-control for app-ssg another', async () => { - const res = await next.fetch('/app-ssg/another') - expect(res.headers.get('cache-control')).toBe( - isNextDev - ? 'no-store, must-revalidate' - : 's-maxage=120, stale-while-revalidate' - ) - }) + ;(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') From afb7d8c1f95f3a863a7a0964a19c85ab3fe8ef4f Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 9 Sep 2024 11:08:17 -0700 Subject: [PATCH 4/5] update test --- .../custom-cache-control/custom-cache-control.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts index 158c953df1681..63e8306dd9b5e 100644 --- a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts +++ b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts @@ -1,10 +1,16 @@ import { nextTestSetup } from 'e2e-utils' describe('custom-cache-control', () => { - const { next, isNextDev } = nextTestSetup({ + 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', () => {}) + } + 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( From 3a4ce3cbc640492cfb9f14defaba7714725c6cf3 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 9 Sep 2024 11:25:36 -0700 Subject: [PATCH 5/5] fix return --- .../app-dir/custom-cache-control/custom-cache-control.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts index 63e8306dd9b5e..dce7b40eb0b80 100644 --- a/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts +++ b/test/e2e/app-dir/custom-cache-control/custom-cache-control.test.ts @@ -9,6 +9,7 @@ describe('custom-cache-control', () => { // 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 () => {