diff --git a/packages/vinext/src/server/dev-server.ts b/packages/vinext/src/server/dev-server.ts index 1bc910d13..439241f30 100644 --- a/packages/vinext/src/server/dev-server.ts +++ b/packages/vinext/src/server/dev-server.ts @@ -583,6 +583,19 @@ export function createSSRHandler( gsspExtraHeaders[key] = String(val); } } + + // Default Cache-Control for getServerSideProps responses, matching + // Next.js's pages-handler.ts (revalidate: 0 → getCacheControlHeader). + // Skip when gSSP already set one via res.setHeader (case-insensitive) + // or when ISR is layered on top below — that branch overwrites this + // default with the ISR cache-control. Fixes #1461. + const hasUserCacheControl = Object.keys(gsspExtraHeaders).some( + (k) => k.toLowerCase() === "cache-control", + ); + if (!hasUserCacheControl) { + gsspExtraHeaders["Cache-Control"] = + "private, no-cache, no-store, max-age=0, must-revalidate"; + } } // Collect font preloads early so ISR cached responses can include // the Link header (font preloads are module-level state that persists diff --git a/packages/vinext/src/server/pages-page-response.ts b/packages/vinext/src/server/pages-page-response.ts index 45bb50921..82a9b4782 100644 --- a/packages/vinext/src/server/pages-page-response.ts +++ b/packages/vinext/src/server/pages-page-response.ts @@ -375,6 +375,11 @@ export async function renderPagesPageResponse( const responseHeaders = new Headers({ "Content-Type": "text/html" }); const finalStatus = applyGsspHeaders(responseHeaders, options.gsspRes); + // Capture user-set Cache-Control (from getServerSideProps's res.setHeader) + // so a downstream user override survives the gssp default below — and only + // the default, never ISR/nonce Cache-Control which the runtime owns. Matches + // Next.js's pages-handler.ts: `if (!res.getHeader('Cache-Control'))`. + const userSetCacheControl = responseHeaders.has("Cache-Control"); if (options.scriptNonce) { responseHeaders.set("Cache-Control", "no-store, must-revalidate"); @@ -384,6 +389,11 @@ export async function renderPagesPageResponse( buildRevalidateCacheControl(options.isrRevalidateSeconds, options.expireSeconds), ); setCacheStateHeaders(responseHeaders, "MISS"); + } else if (options.gsspRes && !userSetCacheControl) { + // Default for getServerSideProps responses, matching Next.js + // pages-handler.ts (revalidate: 0 → getCacheControlHeader). Without this, + // CDNs and browsers could cache per-request gssp responses. + responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate"); } if (options.fontLinkHeader) { responseHeaders.set("Link", options.fontLinkHeader); diff --git a/tests/fixtures/pages-basic/pages/ssr-cache-control.tsx b/tests/fixtures/pages-basic/pages/ssr-cache-control.tsx new file mode 100644 index 000000000..445918d5f --- /dev/null +++ b/tests/fixtures/pages-basic/pages/ssr-cache-control.tsx @@ -0,0 +1,25 @@ +interface Props { + message: string; +} + +// Regression fixture for #1461: a getServerSideProps page that overrides +// Cache-Control via res.setHeader. The override must be forwarded to the +// final HTTP response instead of being replaced by the gssp default. +export default function SSRCacheControlPage({ message }: Props) { + return ( +
{message}
+