diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 1e50f2754a776..d16122ff2ce41 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -3292,7 +3292,10 @@ export default abstract class Server< // 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')) { + if ( + typeof cacheEntry.revalidate !== 'undefined' && + !res.getHeader('Cache-Control') + ) { res.setHeader( 'Cache-Control', formatRevalidate({ @@ -3315,7 +3318,10 @@ export default abstract class Server< } else if (cachedData.kind === CachedRouteKind.REDIRECT) { // 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')) { + if ( + typeof cacheEntry.revalidate !== 'undefined' && + !res.getHeader('Cache-Control') + ) { res.setHeader( 'Cache-Control', formatRevalidate({ @@ -3339,17 +3345,33 @@ export default abstract class Server< return null } } else if (cachedData.kind === CachedRouteKind.APP_ROUTE) { - const headers = { ...cachedData.headers } + const headers = fromNodeOutgoingHttpHeaders(cachedData.headers) if (!(this.minimalMode && isSSG)) { - delete headers[NEXT_CACHE_TAGS_HEADER] + headers.delete(NEXT_CACHE_TAGS_HEADER) + } + + // 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 cacheEntry.revalidate !== 'undefined' && + !res.getHeader('Cache-Control') && + !headers.get('Cache-Control') + ) { + headers.set( + 'Cache-Control', + formatRevalidate({ + revalidate: cacheEntry.revalidate, + expireTime: this.nextConfig.expireTime, + }) + ) } await sendResponse( req, res, new Response(cachedData.body, { - headers: fromNodeOutgoingHttpHeaders(headers), + headers, status: cachedData.status || 200, }) ) diff --git a/test/production/standalone-mode/required-server-files/app/api/test/[slug]/route.js b/test/production/standalone-mode/required-server-files/app/api/test/[slug]/route.js new file mode 100644 index 0000000000000..b0a419c985786 --- /dev/null +++ b/test/production/standalone-mode/required-server-files/app/api/test/[slug]/route.js @@ -0,0 +1,7 @@ +export const dynamic = 'force-static' + +export async function GET(request, context) { + const { params } = context + const { slug } = params + return Response.json({ message: `Hello, ${slug}!` }) +} diff --git a/test/production/standalone-mode/required-server-files/app/test/[slug]/page.jsx b/test/production/standalone-mode/required-server-files/app/test/[slug]/page.jsx new file mode 100644 index 0000000000000..7cd62bbcf0db6 --- /dev/null +++ b/test/production/standalone-mode/required-server-files/app/test/[slug]/page.jsx @@ -0,0 +1,9 @@ +export const revalidate = 3600 + +export default function Page() { + return
Hello, World!
+} + +export async function generateStaticParams() { + return [] +} diff --git a/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts index 0d8507d010651..30a9f3948c2b0 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts @@ -101,6 +101,32 @@ describe('required server files app router', () => { if (server) await killApp(server) }) + it('should send the right cache headers for an app route', async () => { + const res = await fetchViaHTTP(appPort, '/api/test/123', undefined, { + headers: { + 'x-matched-path': '/api/test/[slug]', + 'x-now-route-matches': '1=123&nxtPslug=123', + }, + }) + expect(res.status).toBe(200) + expect(res.headers.get('cache-control')).toBe( + 's-maxage=31536000, stale-while-revalidate' + ) + }) + + it('should send the right cache headers for an app page', async () => { + const res = await fetchViaHTTP(appPort, '/test/123', undefined, { + headers: { + 'x-matched-path': '/test/[slug]', + 'x-now-route-matches': '1=123&nxtPslug=123', + }, + }) + expect(res.status).toBe(200) + expect(res.headers.get('cache-control')).toBe( + 's-maxage=3600, stale-while-revalidate' + ) + }) + it('should not fail caching', async () => { expect(next.cliOutput).not.toContain('ERR_INVALID_URL') }) diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index c02adb1fc1ac9..52d452debc6dd 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -16578,6 +16578,8 @@ "test/production/standalone-mode/required-server-files/required-server-files-app.test.ts": { "passed": [], "failed": [ + "required server files app router should send the right cache headers for an app route", + "required server files app router should send the right cache headers for an app page", "required server files app router should not fail caching", "required server files app router should not send cache tags in minimal mode for SSR", "required server files app router should not send invalid soft tags to cache handler",