Skip to content

fix(app-router): omit provisional RSC cache state for query renders - #2488

Merged
james-elicx merged 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/raw-rsc-page-errors-flight
Jul 2, 2026
Merged

fix(app-router): omit provisional RSC cache state for query renders#2488
james-elicx merged 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/raw-rsc-page-errors-flight

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Overview

Field Details
Goal Avoid advertising provisional RSC cache state for query-bearing App Router RSC requests.
Core change Thread omitPendingDynamicCacheState through RSC finalization and enable it whenever the request has search params.
Main boundary Query-bearing RSC renders can discover searchParams usage only while the Flight stream is consumed, so the response should not expose a provisional cache state first.
Primary files packages/vinext/src/server/app-page-cache-finalizer.ts, packages/vinext/src/server/app-page-dispatch.ts, packages/vinext/src/server/app-page-render.ts
Expected impact Dynamic query renders avoid looking like shared-cache artifacts while their lazy RSC stream is still resolving cache safety.

Why

Cache headers should represent a completed cache decision, not a guess made before stream-time request API usage is known. Query-bearing raw RSC responses are especially sensitive because searchParams access may happen during lazy Flight consumption. This PR reuses the existing pending cache-state omission for RSC query requests so provisional state does not leak to the client.

Area Principle / invariant What this PR changes
RSC cache state A query render should not advertise shared-cache state before dynamic usage is settled. Enables pending cache-state omission for requests with search params, including RSC.
Cache finalization The finalizer owns response cache-state headers. Adds an explicit finalizer option to omit pending cache state while keeping tag behavior.
Dispatch lifecycle Request shape should decide whether provisional cache state is safe. Passes the existing query detection through to render lifecycle for all render transports.

What changed

Scenario Before After
Raw RSC request with query params Could emit provisional pending dynamic cache state before lazy stream usage settled. Omits the provisional cache-state marker while keeping other finalization behavior.
HTML request with query params Already omitted pending dynamic cache state. Still omits it.
Queryless RSC request Existing cache-state behavior applies. Unchanged.
Maintainer review path
File Review focus
packages/vinext/src/server/app-page-dispatch.ts Check the request-level decision that query-bearing requests set omitPendingDynamicCacheState.
packages/vinext/src/server/app-page-render.ts Confirm the lifecycle passes that decision into RSC cache response finalization.
packages/vinext/src/server/app-page-cache-finalizer.ts Verify pending dynamic CDN headers omit only cache state when requested.
tests/app-page-cache.test.ts Review finalizer coverage for cache-state omission.
tests/app-page-render.test.ts Review lifecycle coverage for query-bearing RSC responses.
Validation
  • vp test run tests/app-page-cache.test.ts tests/app-page-render.test.ts
  • vp check packages/vinext/src/server/app-page-cache-finalizer.ts packages/vinext/src/server/app-page-dispatch.ts packages/vinext/src/server/app-page-render.ts tests/app-page-cache.test.ts tests/app-page-render.test.ts
Risk / compatibility
Surface Notes
Public API No public API change.
Config No config change.
Runtime Narrow response-header behavior change for query-bearing App Router RSC requests.
Cache behavior Avoids provisional cache-state exposure; does not change cache writes or cache keys.
Existing apps Low risk. Queryless RSC and existing HTML query behavior are unchanged.
Non-goals
  • Does not change App Router cache key derivation.
  • Does not change render observation semantics.
  • Does not change RSC special-error or page-probe behavior.

@NathanDrake2406
NathanDrake2406 marked this pull request as draft July 2, 2026 14:13
@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2488
npm i https://pkg.pr.new/vinext@2488

commit: 366741a

@NathanDrake2406
NathanDrake2406 force-pushed the nathan/raw-rsc-page-errors-flight branch from db0bd6e to dbad535 Compare July 2, 2026 14:15
@NathanDrake2406 NathanDrake2406 changed the title fix(app-router): keep raw RSC page errors in Flight fix(app-router): omit provisional RSC cache state for query renders Jul 2, 2026
@NathanDrake2406
NathanDrake2406 marked this pull request as ready for review July 2, 2026 14:19
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 366741a against base 6c7b71e using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 126.3 KB 126.3 KB ⚫ -0.0%
Client entry size (gzip) vinext 120.4 KB 120.4 KB ⚫ +0.0%
Dev server cold start vinext 2.54 s 2.53 s ⚫ -0.5%
Production build time vinext 3.12 s 3.11 s ⚫ -0.5%
RSC entry closure size (gzip) vinext 97.9 KB 97.9 KB ⚫ +0.0%
Server bundle size (gzip) vinext 164.4 KB 164.4 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: omit provisional RSC cache state for query renders

The change is small, well-scoped, and correctly reasoned. Verified:

  • Behavioral delta is RSC-only, as documented. For HTML requests, !options.isRscRequest && hasRequestSearchParams was already equal to hasRequestSearchParams (HTML requests have isRscRequest === false), so the HTML path at app-page-render.ts:1123 is genuinely unchanged. Only the RSC finalizer path at app-page-cache-finalizer.ts:239-241 gains the omission.

  • Header semantics are coherent. applyPendingDynamicCdnHeaders runs applyCdnResponseHeaders(..., { pendingDynamicCheck: true }), which the default CDN adapter maps to Cache-Control: no-store, must-revalidate (shims/cdn-cache.ts:149-153). Deleting X-Vinext-Cache/X-Nextjs-Cache when omitCacheState is set leaves a consistent "don't store, no advertised state" response. X-Vinext-Cache is a diagnostic/CDN-state header and is not consumed by client navigation logic, so omitting it is safe.

  • The omission only fires where it matters. In finalizeAppPageRscCacheResponse, the early return at lines 234-236 means omitPendingDynamicCacheState is only applied when preserveClientResponseHeaders !== true, i.e. when rscResponsePolicy.cacheState === "MISS" (app-page-render.ts:866). So this narrows exactly the provisional-MISS case the PR targets and leaves HIT/STALE/skip-transport untouched.

  • hasRequestSearchParams is the right signal. It is guarded by !isForceStatic (app-page-dispatch.ts:551), so force-static routes are correctly excluded from being treated as query-dynamic. There is a single caller of renderAppPageLifecycle, so no other wiring needs updating.

  • Tests are meaningful. Both new tests exercise the real path: the render-lifecycle test gates the stream and flips dynamicUsed during consumption to prove that (a) the provisional state is omitted up front and (b) the cache write is skipped once dynamic usage is discovered. I ran vp test run tests/app-page-cache.test.ts tests/app-page-render.test.ts locally: 86 passed.

No blocking issues. A couple of non-blocking observations below.

const clientHeaders = new Headers(response.headers);
applyPendingDynamicCdnHeaders(clientHeaders, options.getPageTags());
applyPendingDynamicCdnHeaders(clientHeaders, options.getPageTags(), {
omitCacheState: options.omitPendingDynamicCacheState === true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the omission is only meaningful here because this branch runs with pendingDynamicCheck: true (Cache-Control becomes no-store, must-revalidate). A one-line comment noting that invariant — that deleting the state headers is safe specifically in the pending-dynamic/no-store case — would help a future reader understand why this does not leave a stored-but-untagged response.

handlerStart: options.handlerStart,
hasLoadingBoundary: hasActiveLoadingBoundary,
omitPendingDynamicCacheState: !options.isRscRequest && hasRequestSearchParams,
omitPendingDynamicCacheState: hasRequestSearchParams,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed this now covers RSC requests too. Since AGENTS.md requires verifying Next.js parity for cache-behavior changes and there is no .nextjs-ref clone available here, it would be worth confirming (and noting in the PR description) that Next.js also does not advertise a shared-cache state on query-bearing raw RSC (.rsc) responses whose searchParams usage is only discovered during Flight consumption. The reasoning in the PR is sound (searchParams access can happen during lazy stream consumption), but a link to the corresponding Next.js source/test would lock in the parity claim per the repo guidelines.

@ask-bonk

ask-bonk Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #2488.

Summary

I reviewed PR #2488 (fix(app-router): omit provisional RSC cache state for query renders) and posted a review with an approving assessment and two non-blocking observations.

Verdict: no blocking issues. The change is small, well-scoped, and correctly reasoned.

Key findings from my analysis:

  1. Behavioral delta is RSC-only — For HTML requests, the old condition !isRscRequest && hasRequestSearchParams already equaled hasRequestSearchParams, so HTML behavior is genuinely unchanged. Only RSC query renders gain the omission, matching the PR description.

  2. Header semantics are coherentpendingDynamicCheck: true sets Cache-Control: no-store, must-revalidate, and deleting the X-Vinext-Cache/X-Nextjs-Cache diagnostic headers leaves a consistent "don't store, no advertised state" response. Those headers aren't consumed by client navigation.

  3. The omission is correctly narrowed — The early return in finalizeAppPageRscCacheResponse means it only applies when cacheState === "MISS", exactly the provisional case targeted; HIT/STALE/skip-transport are untouched.

  4. hasRequestSearchParams is the right signal — guarded by !isForceStatic, single caller of the lifecycle.

  5. Tests are meaningful — I ran them locally: 86 passed.

The two non-blocking comments suggest: (a) a clarifying comment about the pending-dynamic/no-store invariant, and (b) documenting the Next.js parity confirmation per AGENTS.md guidelines (no .nextjs-ref clone was available to verify directly).

github run

@james-elicx
james-elicx merged commit a9a30a5 into cloudflare:main Jul 2, 2026
52 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 2, 2026
@NathanDrake2406
NathanDrake2406 deleted the nathan/raw-rsc-page-errors-flight branch July 3, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants