diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index 544b6f844c4f..3600f5b7336d 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -999,7 +999,12 @@ export async function handler( // In dev, we should not cache pages for any reason. if (routeModule.isDev) { - res.setHeader('Cache-Control', 'no-store, must-revalidate') + res.setHeader( + 'Cache-Control', + nextConfig.experimental.devCacheControlNoCache + ? 'no-cache, must-revalidate' + : 'no-store, must-revalidate' + ) } if (!cacheEntry) { diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index a58c1750ce3d..28bc5cd18455 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1782,7 +1782,12 @@ export default abstract class Server< // In dev, we should not cache pages for any reason. if (dev) { - res.setHeader('Cache-Control', 'no-store, must-revalidate') + res.setHeader( + 'Cache-Control', + this.nextConfig.experimental.devCacheControlNoCache + ? 'no-cache, must-revalidate' + : 'no-store, must-revalidate' + ) cacheControl = undefined } diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 1e066bf047b1..4a3f925f47c0 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -352,6 +352,7 @@ export const experimentalSchema = { lockDistDir: z.boolean().optional(), hideLogsAfterAbort: z.boolean().optional(), runtimeServerDeploymentId: z.boolean().optional(), + devCacheControlNoCache: z.boolean().optional(), } export const configSchema: zod.ZodType = z.lazy(() => diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 4a5bb64b585c..11a921895a9d 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -873,6 +873,18 @@ export interface ExperimentalConfig { * @default false */ runtimeServerDeploymentId?: boolean + + /** + * Use 'no-cache' instead of 'no-store' in the Cache-Control header for development. + * This allows conditional requests to the server, which can help with development + * workflows that benefit from caching validation. + * + * When enabled, the Cache-Control header changes from 'no-store, must-revalidate' + * to 'no-cache, must-revalidate'. + * + * @default false + */ + devCacheControlNoCache?: boolean } export type ExportPathMap = { @@ -1599,6 +1611,7 @@ export const defaultConfig = Object.freeze({ turbopackFileSystemCacheForDev: true, turbopackFileSystemCacheForBuild: false, turbopackInferModuleSideEffects: !isStableBuild(), + devCacheControlNoCache: false, }, htmlLimitedBots: undefined, bundlePagesRouterDependencies: false, @@ -1695,6 +1708,7 @@ export interface NextConfigRuntime { | 'testProxy' | 'runtimeServerDeploymentId' | 'maxPostponedStateSize' + | 'devCacheControlNoCache' > & { // Pick on @internal fields generates invalid .d.ts files /** @internal */ @@ -1752,6 +1766,7 @@ export function getNextConfigRuntime( testProxy: ex.testProxy, runtimeServerDeploymentId: ex.runtimeServerDeploymentId, maxPostponedStateSize: ex.maxPostponedStateSize, + devCacheControlNoCache: ex.devCacheControlNoCache, trustHostHeader: ex.trustHostHeader, isExperimentalCompile: ex.isExperimentalCompile, diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index ea8c8e266189..6c606c34ea4a 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -497,7 +497,12 @@ export async function initialize(opts: { matchedOutput.type === 'nextStaticFolder' ) { if (opts.dev && !isNextFont(parsedUrl.pathname)) { - res.setHeader('Cache-Control', 'no-store, must-revalidate') + res.setHeader( + 'Cache-Control', + config.experimental.devCacheControlNoCache + ? 'no-cache, must-revalidate' + : 'no-store, must-revalidate' + ) } else { res.setHeader( 'Cache-Control', diff --git a/packages/next/src/server/route-modules/pages/pages-handler.ts b/packages/next/src/server/route-modules/pages/pages-handler.ts index a7b3acd60bdb..93dbb5515b02 100644 --- a/packages/next/src/server/route-modules/pages/pages-handler.ts +++ b/packages/next/src/server/route-modules/pages/pages-handler.ts @@ -684,7 +684,12 @@ export const getHandler = ({ // In dev, we should not cache pages for any reason. if (routeModule.isDev) { - res.setHeader('Cache-Control', 'no-store, must-revalidate') + res.setHeader( + 'Cache-Control', + nextConfig.experimental.devCacheControlNoCache + ? 'no-cache, must-revalidate' + : 'no-store, must-revalidate' + ) } // Draft mode should never be cached diff --git a/test/development/dev-cache-control-no-cache-disabled/app/app-route/page.js b/test/development/dev-cache-control-no-cache-disabled/app/app-route/page.js new file mode 100644 index 000000000000..cabb5263b521 --- /dev/null +++ b/test/development/dev-cache-control-no-cache-disabled/app/app-route/page.js @@ -0,0 +1,3 @@ +export default function AppRoute() { + return
App Route
+} diff --git a/test/development/dev-cache-control-no-cache-disabled/app/layout.js b/test/development/dev-cache-control-no-cache-disabled/app/layout.js new file mode 100644 index 000000000000..4ee00a218505 --- /dev/null +++ b/test/development/dev-cache-control-no-cache-disabled/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/development/dev-cache-control-no-cache-disabled/dev-cache-control-no-cache-disabled.test.ts b/test/development/dev-cache-control-no-cache-disabled/dev-cache-control-no-cache-disabled.test.ts new file mode 100644 index 000000000000..5f1cf1f12fa7 --- /dev/null +++ b/test/development/dev-cache-control-no-cache-disabled/dev-cache-control-no-cache-disabled.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('experimental.devCacheControlNoCache disabled (default)', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use no-store for pages router by default', async () => { + const res = await next.fetch('/pages-route') + expect(res.headers.get('Cache-Control')).toBe('no-store, must-revalidate') + }) + + it('should use no-store for app router by default', async () => { + const res = await next.fetch('/app-route') + expect(res.headers.get('Cache-Control')).toBe('no-store, must-revalidate') + }) +}) diff --git a/test/development/dev-cache-control-no-cache-disabled/next.config.js b/test/development/dev-cache-control-no-cache-disabled/next.config.js new file mode 100644 index 000000000000..5a877d2dbfab --- /dev/null +++ b/test/development/dev-cache-control-no-cache-disabled/next.config.js @@ -0,0 +1,2 @@ +/** @type {import('next').NextConfig} */ +module.exports = {} diff --git a/test/development/dev-cache-control-no-cache-disabled/pages/pages-route.js b/test/development/dev-cache-control-no-cache-disabled/pages/pages-route.js new file mode 100644 index 000000000000..80120c33615c --- /dev/null +++ b/test/development/dev-cache-control-no-cache-disabled/pages/pages-route.js @@ -0,0 +1,3 @@ +export default function PagesRoute() { + return
Pages Route
+} diff --git a/test/development/dev-cache-control-no-cache/app/app-route/page.js b/test/development/dev-cache-control-no-cache/app/app-route/page.js new file mode 100644 index 000000000000..cabb5263b521 --- /dev/null +++ b/test/development/dev-cache-control-no-cache/app/app-route/page.js @@ -0,0 +1,3 @@ +export default function AppRoute() { + return
App Route
+} diff --git a/test/development/dev-cache-control-no-cache/app/layout.js b/test/development/dev-cache-control-no-cache/app/layout.js new file mode 100644 index 000000000000..4ee00a218505 --- /dev/null +++ b/test/development/dev-cache-control-no-cache/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/development/dev-cache-control-no-cache/dev-cache-control-no-cache.test.ts b/test/development/dev-cache-control-no-cache/dev-cache-control-no-cache.test.ts new file mode 100644 index 000000000000..edbe93714b89 --- /dev/null +++ b/test/development/dev-cache-control-no-cache/dev-cache-control-no-cache.test.ts @@ -0,0 +1,19 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('experimental.devCacheControlNoCache', () => { + describe('when enabled', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use no-cache instead of no-store for pages router', async () => { + const res = await next.fetch('/pages-route') + expect(res.headers.get('Cache-Control')).toBe('no-cache, must-revalidate') + }) + + it('should use no-cache instead of no-store for app router', async () => { + const res = await next.fetch('/app-route') + expect(res.headers.get('Cache-Control')).toBe('no-cache, must-revalidate') + }) + }) +}) diff --git a/test/development/dev-cache-control-no-cache/next.config.js b/test/development/dev-cache-control-no-cache/next.config.js new file mode 100644 index 000000000000..0f939a1f9b51 --- /dev/null +++ b/test/development/dev-cache-control-no-cache/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + experimental: { + devCacheControlNoCache: true, + }, +} diff --git a/test/development/dev-cache-control-no-cache/pages/pages-route.js b/test/development/dev-cache-control-no-cache/pages/pages-route.js new file mode 100644 index 000000000000..80120c33615c --- /dev/null +++ b/test/development/dev-cache-control-no-cache/pages/pages-route.js @@ -0,0 +1,3 @@ +export default function PagesRoute() { + return
Pages Route
+}