diff --git a/packages/vinext/src/server/pages-page-handler.ts b/packages/vinext/src/server/pages-page-handler.ts index fac8f96a5..2f5697213 100644 --- a/packages/vinext/src/server/pages-page-handler.ts +++ b/packages/vinext/src/server/pages-page-handler.ts @@ -27,7 +27,7 @@ import { hasUserDocumentGetInitialProps } from "./document-initial-head.js"; import { mergePagesNotFoundSourceHeaders, resolvePagesPageData } from "./pages-page-data.js"; import type { PagesPageModule } from "./pages-page-data.js"; import { resolvePagesPageMethodResponse } from "./pages-page-method.js"; -import { renderPagesPageResponse } from "./pages-page-response.js"; +import { applyGsspResponseHeaders, renderPagesPageResponse } from "./pages-page-response.js"; import { buildPagesReadinessNextData } from "./pages-readiness.js"; import type { PagesI18nRenderContext } from "./pages-page-response.js"; import type { RenderPageEnhancers } from "./pages-document-initial-props.js"; @@ -984,42 +984,26 @@ export function createPagesPageHandler( // and expects the full props envelope (pageProps plus any app-level // props like __N_SSP, __N_SSG) as JSON instead of the full HTML page. if (isDataReq) { - const init: ResponseInit & { headers: Record } = { headers: {} }; - if (gsspRes && typeof gsspRes.getHeaders === "function") { - const gsspHeaders = gsspRes.getHeaders(); - for (const k of Object.keys(gsspHeaders)) { - const v = gsspHeaders[k]; - if (v === undefined || v === null) continue; - init.headers[k] = Array.isArray(v) ? v.join(", ") : String(v); - } - } + const headers = new Headers({ "Content-Type": "application/json" }); + applyGsspResponseHeaders(headers, gsspRes); + headers.set("Content-Type", "application/json"); + const status = gsspRes?.statusCode ?? 200; if (gsspRes) { // Default Cache-Control for gSSP-driven _next/data responses — // skip when gSSP already set one via res.setHeader. Fixes #1461. - let hasUserCacheControl = false; - for (const headerKey of Object.keys(init.headers)) { - if (headerKey.toLowerCase() === "cache-control") { - hasUserCacheControl = true; - break; - } - } - if (!hasUserCacheControl) { - init.headers["Cache-Control"] = ISR_NEVER_CACHE_CONTROL; + if (!headers.has("Cache-Control")) { + headers.set("Cache-Control", ISR_NEVER_CACHE_CONTROL); } } else if (isStaticPropsRoute) { if (isrRevalidateSeconds !== null) { - const headers = new Headers(init.headers); applyCdnResponseHeaders(headers, { cacheControl: buildMissIsrCacheControl( isrRevalidateSeconds, vinextConfig.expireTime, ), }); - for (const [key, value] of headers) { - init.headers[key] = value; - } } else if (shouldUseNextDeployCacheControl()) { - init.headers["Cache-Control"] = BROWSER_REVALIDATE_CACHE_CONTROL; + headers.set("Cache-Control", BROWSER_REVALIDATE_CACHE_CONTROL); } } // Mirror Next.js pages-handler.ts: set x-nextjs-deployment-id on @@ -1031,11 +1015,11 @@ export function createPagesPageHandler( const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID; if (deploymentId) { - init.headers[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId; + headers.set(NEXTJS_DEPLOYMENT_ID_HEADER, deploymentId); } } return finalizePagesPreviewResponse( - buildNextDataPropsJsonResponse(renderProps, safeJsonStringify, init), + buildNextDataPropsJsonResponse(renderProps, safeJsonStringify, { headers, status }), preview, ); } diff --git a/packages/vinext/src/server/pages-page-response.ts b/packages/vinext/src/server/pages-page-response.ts index da0d509e0..45256a2c9 100644 --- a/packages/vinext/src/server/pages-page-response.ts +++ b/packages/vinext/src/server/pages-page-response.ts @@ -458,15 +458,11 @@ function schedulePagesIsrCacheWrite(options: Parameters { expect(context?.asPath).toBe("/about?x=1"); }); + it("preserves gSSP status and individual cookies on data responses", async () => { + const cookies = [ + "session=expired; Expires=Wed, 21 Oct 2037 07:28:00 GMT; Path=/", + "notice=reauthenticate; Path=/", + ]; + const routes = [ + makeRoute( + "/about", + makePageModule({ + getServerSideProps: async ({ + res, + }: { + res: { + statusCode: number; + setHeader(name: string, value: string | string[]): void; + }; + }) => { + res.statusCode = 401; + res.setHeader("Set-Cookie", cookies); + return { props: {} }; + }, + }), + ), + ]; + const handler = createPagesPageHandler(makeOpts({ pageRoutes: routes })); + const dataUrl = "/_next/data/test-build-id/about.json"; + + const response = await handler(makeRequest(dataUrl), dataUrl, null, null, null); + + expect(response.status).toBe(401); + expect(response.headers.getSetCookie()).toEqual(cookies); + expect(response.headers.get("content-type")).toContain("application/json"); + }); + it("marks preview data and forces private no-store caching", async () => { const routeModule = makePageModule({ getStaticProps: async ({ previewData }: { previewData: unknown }) => ({