Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
62dec94
fix(pages): prevent request props from entering ISR
NathanDrake2406 Aug 23, 2026
b0e8fbe
test(pages): expect ISR bypass for request-aware app props
NathanDrake2406 Aug 23, 2026
e3f1456
fix(pages): tighten request-aware App ISR bypass
NathanDrake2406 Aug 24, 2026
c8d6046
fix(pages): no-store request-aware GSP responses and keep revalidate …
NathanDrake2406 Aug 24, 2026
3e7a8d4
fix(pages): no-store request-aware fallback shells and clear edge cac…
NathanDrake2406 Aug 24, 2026
b414ff9
fix(pages): clear edge cache headers on nonce-bearing bypass renders
NathanDrake2406 Aug 24, 2026
59f6034
fix(pages): close request-aware ISR bypass gaps
NathanDrake2406 Aug 24, 2026
ba194af
fix(pages): enforce request-aware response boundaries
NathanDrake2406 Aug 24, 2026
14d6c46
fix(pages): preserve no-store through final request paths
NathanDrake2406 Aug 24, 2026
adba844
fix(pages): retain provider no-store policies
NathanDrake2406 Aug 24, 2026
abb5ad7
fix(pages): preserve cache and export contracts
NathanDrake2406 Aug 24, 2026
3f8cc1f
fix(pages): close dynamic response cache exits
NathanDrake2406 Aug 24, 2026
0e63b3d
fix(pages): reject non-cacheable static exports
NathanDrake2406 Aug 24, 2026
5e059d1
fix(pages): fail incompatible prerender builds
NathanDrake2406 Aug 24, 2026
20772e7
fix(pages): close prerender eligibility gaps
NathanDrake2406 Aug 24, 2026
254618d
fix(pages): validate exports before static path lookup
NathanDrake2406 Aug 24, 2026
d9fd0d3
Merge remote-tracking branch 'origin/main' into codex/pr3067-owner
james-elicx Sep 9, 2026
02cfb98
Merge remote-tracking branch 'origin/main' into codex/pr3067-owner
james-elicx Sep 9, 2026
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
58 changes: 48 additions & 10 deletions packages/vinext/src/server/pages-page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
CacheControlMetadata,
} from "vinext/shims/cache-handler";
import { applyCdnResponseHeaders } from "./cache-control.js";
import { buildMissIsrCacheControl, decideIsr } from "./isr-decision.js";
import { buildMissIsrCacheControl, decideIsr, ISR_NEVER_CACHE_CONTROL } from "./isr-decision.js";
import { buildCacheStateHeaders } from "./cache-headers.js";
import {
buildPagesCacheValue,
Expand Down Expand Up @@ -382,6 +382,11 @@ type ResolvePagesPageDataRenderResult = {
gsspRes: PagesGsspResponse | null;
isrRevalidateSeconds: number | false | null;
isrExpireSeconds?: number;
/**
* True when a getStaticProps render carries request-derived App props. The
* caller must emit a private, no-store policy so no shared cache stores it.
*/
bypassSharedCache: boolean;
pageProps: Record<string, unknown>;
props: PagesRenderProps;
/**
Expand Down Expand Up @@ -482,6 +487,16 @@ export function mergePagesNotFoundSourceHeaders(
});
}

/**
* True only when userland overrode App.getInitialProps. The shim's inherited
* default (`class MyApp extends App {}`) is request-agnostic and keeps ISR.
*/
function hasCustomAppGetInitialProps(appComponent: unknown): boolean {
if (!hasPagesGetInitialProps(appComponent)) return false;
const component = appComponent as { getInitialProps?: unknown; origGetInitialProps?: unknown };
return component.getInitialProps !== component.origGetInitialProps;
Comment thread
NathanDrake2406 marked this conversation as resolved.
Outdated
}

function applyPagesTerminalMissHeaders(
response: Response,
revalidateSeconds: number | false,
Expand Down Expand Up @@ -1210,6 +1225,10 @@ export async function resolvePagesPageData(
let shouldPersistFallbackData = false;
let onDemandPreviousCacheEntry: ISRCacheEntry | null | undefined;
const previewData = options.isOnDemandRevalidate ? false : (options.previewData ?? false);
// A user-defined App.getInitialProps receives the live request. Its result
// can contain cookies, headers, or other per-user data and must never enter
// (or be read from) the shared ISR cache used by getStaticProps pages.
const hasRequestAwareAppProps = hasCustomAppGetInitialProps(options.AppComponent);
Comment thread
NathanDrake2406 marked this conversation as resolved.

if (typeof options.pageModule.getStaticPaths === "function" && options.route.isDynamic) {
const pathsResult = await options.pageModule.getStaticPaths({
Expand Down Expand Up @@ -1260,7 +1279,11 @@ export async function resolvePagesPageData(
options.revalidateOnlyGenerated
) {
const pathname = options.isrCachePathname ?? options.routeUrl.split("?")[0];
onDemandPreviousCacheEntry = await options.isrGet(options.isrCacheKey("pages", pathname));
// Request-aware App props never produce a shared entry, so there is
// nothing generated to revalidate.
onDemandPreviousCacheEntry = hasRequestAwareAppProps
? null
: await options.isrGet(options.isrCacheKey("pages", pathname));
if (!onDemandPreviousCacheEntry) {
return {
kind: "response",
Expand Down Expand Up @@ -1318,6 +1341,7 @@ export async function resolvePagesPageData(
documentReqRes: sharedReqRes,
gsspRes: null,
isrRevalidateSeconds: null,
bypassSharedCache: hasRequestAwareAppProps,
Comment thread
NathanDrake2406 marked this conversation as resolved.
pageProps,
props: renderProps,
isFallback: true,
Expand Down Expand Up @@ -1405,8 +1429,9 @@ export async function resolvePagesPageData(
if (typeof options.pageModule.getStaticProps === "function") {
const pathname = options.isrCachePathname ?? options.routeUrl.split("?")[0];
const cacheKey = options.isrCacheKey("pages", pathname);
const cached =
onDemandPreviousCacheEntry !== undefined
const cached = hasRequestAwareAppProps
? null
: onDemandPreviousCacheEntry !== undefined
? onDemandPreviousCacheEntry
: await options.isrGet(cacheKey);
const cachedValue = cached?.value.value;
Expand Down Expand Up @@ -1722,9 +1747,12 @@ export async function resolvePagesPageData(

if (result?.redirect) {
const response = buildPagesRedirectResponse(result.redirect, options, renderProps);
if (previewData === false) {
const revalidateSeconds = resolvePagesRevalidateSeconds(result, options.routeUrl);
const expireSeconds = resolvePagesExpireSeconds(result, options.expireSeconds);
// Validate `revalidate` even when the result never enters the cache.
const revalidateSeconds = resolvePagesRevalidateSeconds(result, options.routeUrl);
const expireSeconds = resolvePagesExpireSeconds(result, options.expireSeconds);
if (hasRequestAwareAppProps) {
applyCdnResponseHeaders(response.headers, { cacheControl: ISR_NEVER_CACHE_CONTROL });
} else if (previewData === false) {
const redirect = resolvePagesRedirect(result.redirect, {
method: "getStaticProps",
routeUrl: options.routeUrl,
Expand All @@ -1749,6 +1777,11 @@ export async function resolvePagesPageData(
if (result?.notFound) {
const revalidateSeconds = resolvePagesRevalidateSeconds(result, options.routeUrl);
const expireSeconds = resolvePagesExpireSeconds(result, options.expireSeconds);
// The recursive custom 404 render also runs App.getInitialProps, so
// request-aware App props must not receive a shared cache lifetime either.
if (hasRequestAwareAppProps) {
return buildPagesNotFoundResult(options);
}
if (previewData === false) {
await options.isrSet(cacheKey, null, {
cacheControl: isrCacheControl(revalidateSeconds, { expireSeconds }),
Expand Down Expand Up @@ -1783,10 +1816,13 @@ export async function resolvePagesPageData(
isSerializableProps(options.routePattern, "getStaticProps", pageProps);
}

if (previewData === false && result) {
if (previewData === false && result && !hasRequestAwareAppProps) {
isrRevalidateSeconds = resolvePagesRevalidateSeconds(result, options.routeUrl);
isrExpireSeconds = resolvePagesExpireSeconds(result, options.expireSeconds);
Comment thread
NathanDrake2406 marked this conversation as resolved.
} else if (previewData === false && options.isOnDemandRevalidate) {
} else if (result && hasRequestAwareAppProps) {
// Still validate `revalidate`; only cache participation is skipped.
resolvePagesRevalidateSeconds(result, options.routeUrl);
} else if (previewData === false && options.isOnDemandRevalidate && !hasRequestAwareAppProps) {
Comment thread
NathanDrake2406 marked this conversation as resolved.
// `revalidate: false` (and an omitted `revalidate`) still participates in
// on-demand regeneration. Persist the current invocation's normalized
// lifetime instead of inheriting stale metadata from the previous entry.
Expand All @@ -1800,7 +1836,7 @@ export async function resolvePagesPageData(
isrExpireSeconds = cached?.value.cacheControl?.expire;
}

if (shouldPersistFallbackData && previewData === false) {
if (shouldPersistFallbackData && previewData === false && !hasRequestAwareAppProps) {
const revalidateSeconds = isrRevalidateSeconds ?? false;
await options.isrSet(
cacheKey,
Expand Down Expand Up @@ -1866,6 +1902,8 @@ export async function resolvePagesPageData(
gsspRes,
isrRevalidateSeconds,
isrExpireSeconds,
bypassSharedCache:
Comment thread
NathanDrake2406 marked this conversation as resolved.
hasRequestAwareAppProps && typeof options.pageModule.getStaticProps === "function",
Comment thread
NathanDrake2406 marked this conversation as resolved.
Comment thread
NathanDrake2406 marked this conversation as resolved.
pageProps,
props: renderProps,
isFallback: false,
Expand Down
6 changes: 6 additions & 0 deletions packages/vinext/src/server/pages-page-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ export function createPagesPageHandler(
// response and must not shorten `/404`'s internal cache lifetime.
const isrRevalidateSeconds = pageDataResult.isrRevalidateSeconds;
const isrExpireSeconds = pageDataResult.isrExpireSeconds;
const bypassSharedCache = pageDataResult.bypassSharedCache;
const isFallbackRender = pageDataResult.isFallback === true;

// Republish SSR context with isFallback flipped on so `useRouter().isFallback`
Expand Down Expand Up @@ -991,6 +992,10 @@ export function createPagesPageHandler(
if (!hasUserCacheControl) {
init.headers["Cache-Control"] = ISR_NEVER_CACHE_CONTROL;
}
} else if (bypassSharedCache) {
const headers = new Headers(init.headers);
applyCdnResponseHeaders(headers, { cacheControl: ISR_NEVER_CACHE_CONTROL });
init.headers = Object.fromEntries(headers);
} else if (isStaticPropsRoute) {
if (isrRevalidateSeconds !== null) {
const headers = new Headers(init.headers);
Expand Down Expand Up @@ -1074,6 +1079,7 @@ export function createPagesPageHandler(
isrCachePathname,
expireSeconds: isrExpireSeconds,
isrRevalidateSeconds,
bypassSharedCache,
Comment thread
NathanDrake2406 marked this conversation as resolved.
isOnDemandRevalidate,
isStaticPropsRoute,
isrSet,
Expand Down
6 changes: 6 additions & 0 deletions packages/vinext/src/server/pages-page-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ type RenderPagesPageResponseOptions = {
isrCachePathname?: string;
expireSeconds?: number;
isrRevalidateSeconds: number | false | null;
/** Request-derived App props: emit a private, no-store policy instead of ISR headers. */
bypassSharedCache?: boolean;
/** Synchronous `res.revalidate()` render; cache persistence must finish before returning. */
isOnDemandRevalidate?: boolean;
isStaticPropsRoute?: boolean;
Expand Down Expand Up @@ -699,6 +701,10 @@ export async function renderPagesPageResponse(

if (options.scriptNonce) {
responseHeaders.set("Cache-Control", ISR_NO_STORE_CACHE_CONTROL);
} else if (options.bypassSharedCache) {
Comment thread
NathanDrake2406 marked this conversation as resolved.
Outdated
// Route through the adapter so provider-owned edge headers set by
// App.getInitialProps are cleared as well.
applyCdnResponseHeaders(responseHeaders, { cacheControl: ISR_NEVER_CACHE_CONTROL });
} else if (options.isrRevalidateSeconds !== null) {
// Fresh ISR (MISS) response: route through the CDN adapter with the path tag
// used by Pages Router invalidation while the default emits Cache-Control.
Expand Down
Loading
Loading