Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 10 additions & 26 deletions packages/vinext/src/server/pages-page-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, string> } = { 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
Expand All @@ -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,
);
}
Expand Down
22 changes: 15 additions & 7 deletions packages/vinext/src/server/pages-page-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,15 +458,11 @@ function schedulePagesIsrCacheWrite(options: Parameters<typeof writePagesIsrCach
getRequestExecutionContext()?.waitUntil(cacheWritePromise);
}

function applyGsspHeaders(
export function applyGsspResponseHeaders(
headers: Headers,
gsspRes: PagesGsspResponse | null,
statusCode?: number,
): number {
if (!gsspRes) {
return statusCode ?? 200;
}

): void {
if (!gsspRes) return;
const gsspHeaders = gsspRes.getHeaders();
for (const key of Object.keys(gsspHeaders)) {
const value = gsspHeaders[key];
Expand All @@ -485,6 +481,18 @@ function applyGsspHeaders(
headers.set(key, String(value));
}
}
}

function applyGsspHeaders(
headers: Headers,
gsspRes: PagesGsspResponse | null,
statusCode?: number,
): number {
if (!gsspRes) {
return statusCode ?? 200;
}

applyGsspResponseHeaders(headers, gsspRes);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/html; charset=utf-8");
}
Expand Down
34 changes: 34 additions & 0 deletions tests/pages-page-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,40 @@ describe("createPagesPageHandler — _next/data", () => {
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 }) => ({
Expand Down