Skip to content

feat(app-router): implement RSC segment cache prefetch protocol#1420

Open
yunus25jmi1 wants to merge 4 commits into
cloudflare:mainfrom
yunus25jmi1:feat/segment-cache-prefetch-protocol
Open

feat(app-router): implement RSC segment cache prefetch protocol#1420
yunus25jmi1 wants to merge 4 commits into
cloudflare:mainfrom
yunus25jmi1:feat/segment-cache-prefetch-protocol

Conversation

@yunus25jmi1
Copy link
Copy Markdown
Contributor

Problem

vinext does not implement the Next.js 15+ RSC segment cache prefetch protocol. The client-side router expects to make segment-level prefetch requests (with Next-Router-Segment-Prefetch header) to URLs in the format /page-path.segments/_tree.segment.rsc and receive segment-specific RSC payloads.

Without this endpoint, the "Next.js Deploy Suite" (#1328) tests time out waiting for segment-prefetch requests that never fire. Estimated impact: ~66 test failures across the deploy suite.

Fixes #1335.

What this PR implements (Phase 1)

This adds the server-side URL handling for the segment cache prefetch protocol. When a request arrives at .segments/*.segment.rsc, the handler:

  1. Normalizes the URL back to the original page path (e.g., /dashboard.segments/_tree.segment.rsc -> pathname=/dashboard, segmentPath=/_tree)
  2. Marks the request as an RSC prefetch (isRscRequest=true, segmentPrefetchPath set)
  3. Routes through the normal RSC pipeline — the page renders and returns valid RSC content

The client segment cache receives the full-page RSC response and falls through to the NavigationFlightResponse code path, which correctly handles it.

Files changed

File Change
packages/vinext/src/server/app-segment-prefetch-normalizer.ts NEW — matches .segments/*.segment.rsc URLs, extracts original path + segment path
packages/vinext/src/server/app-rsc-request-normalization.ts Adds segment-prefetch detection to the normalization pipeline
packages/vinext/src/server/app-rsc-handler.ts Threads segmentPrefetchPath through DispatchMatchedPageOptions
packages/vinext/src/entries/app-rsc-entry.ts Accepts segmentPrefetchPath in generated dispatchMatchedPage
tests/app-segment-prefetch-normalizer.test.ts NEW — 22 unit tests for URL matching/extraction
tests/app-rsc-request-normalization.test.ts 7 new tests for normalization of segment-prefetch URLs

Not in this PR (follow-up)

  • Segment-level response generation: Returning proper RootTreePrefetch / SegmentPrefetchResponse payloads requires rendering the page and walking the RSC output tree to extract per-segment data. This is deferred as Phase 2.
  • Client-side segment cache: The scheduler, queue, CacheMap with fallback vary-param lookup, memory pressure eviction, etc.
  • Next-Did-Postpone: 2 header: Intentionally omitted. Without it, the client falls through to the non-PPR code path that correctly processes full-page RSC payloads.
  • Build-time segment pre-computation: Next.js computes segment data at build time in collectSegmentData. vinext can do the same once the runtime protocol is solid.

Trade-offs

  • Minimal but correct: The endpoint exists, responds with valid text/x-component content, and doesn't error. This is sufficient for the deploy suite tests that only check for request initiation.
  • No granular caching: Returning full-page RSC for all segment-prefetch requests is functionally correct but doesn't provide the caching benefits of per-segment payloads.
  • No Next-Did-Postpone: 2: Setting this would cause the client to attempt RootTreePrefetch parsing. Omitting it allows graceful fallback to the existing prefetch format.
  • Deferred complexity: Complex segment-cache E2E tests should be skipped with adapter-api-e2e annotations until Phase 2+ is ready.

Testing

  • 22 unit tests for the URL normalizer (match + extract)
  • 7 integration tests for the request normalization pipeline
  • All existing tests pass:
    • 6 core test files: 251/251 passed
    • app-router.test.ts: 313/313 passed
    • features.test.ts: 292/292 passed
    • Build: clean

Closes: #1335
Related: #1328

@yunus25jmi1 yunus25jmi1 force-pushed the feat/segment-cache-prefetch-protocol branch from 88e9125 to 6d947cb Compare May 22, 2026 05:11
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1420

commit: c0eb026

@yunus25jmi1 yunus25jmi1 force-pushed the feat/segment-cache-prefetch-protocol branch from 6d947cb to 55b9be7 Compare May 22, 2026 05:18
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review: RSC segment cache prefetch protocol (Phase 1)

The normalizer and normalization pipeline changes are clean. The URL pattern matching is correct, tests are thorough, and the architecture decision to return full-page RSC (falling through to NavigationFlightResponse) is a pragmatic Phase 1. A few things to address before merging.

Issues

  1. Double regex executionmatchSegmentPrefetchRsc + extractSegmentPrefetchRsc run the same regex twice. Since the extract function already returns null for non-matches, the match guard is redundant.

  2. Inline comment removal — The inline step comments (// Step 2:, // Step 3:, etc.) in normalizeRscRequest were useful — they tied the code to the numbered steps in the function-level doc block, making it easy to audit security-sensitive ordering. The doc block steps were renumbered, but the inline anchors are gone. Please keep them.

  3. Doc-comment removal on NormalizedRscRequest fields — The existing per-field JSDoc comments were removed (/** Parsed URL. Callers may mutate... */, /** True when the request targets... */, etc.). These were useful for type hover docs. Please keep them alongside the new segmentPrefetchPath field.

  4. segmentPrefetchPath is threaded but unused — The value propagates through DispatchMatchedPageOptions → generated entry destructure → ... and is never consumed. This is fine for Phase 1 plumbing, but worth noting that the type contract now has a field no consumer reads.

  5. Missing negative test: percent-encoded .segments — What happens when .segments is percent-encoded (e.g., %2Esegments)? The strict decoder runs before segment-prefetch detection, so %2E decodes to ., meaning the regex would match. This might be fine (same behavior as .rsc suffix detection), but it should have a test documenting the expected behavior.

segmentPrefetchPath = extracted.segmentPath;
pathname = extracted.originalPathname;
}
}
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.

The matchSegmentPrefetchRsc guard is redundant — extractSegmentPrefetchRsc already returns null for non-matches, so the if (extracted) check handles it. Running the regex twice on every segment-prefetch request is wasteful on a hot path.

Suggested change
}
let segmentPrefetchPath: string | null = null;
const extracted = extractSegmentPrefetchRsc(pathname);
if (extracted) {
segmentPrefetchPath = extracted.segmentPath;
pathname = extracted.originalPathname;
}

/** Parsed URL. Callers may mutate `url.search` after middleware runs. */
url: URL;
/** Normalized pathname with basePath stripped. Used for all internal routing. */
pathname: string;
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.

The per-field JSDoc comments on NormalizedRscRequest were removed in this PR. These were useful for IDE hover docs:

/** Parsed URL. Callers may mutate `url.search` after middleware runs. */
url: URL;
/** Normalized pathname with basePath stripped. Used for all internal routing. */
pathname: string;

etc. Please keep them — the new segmentPrefetchPath doc is good, just don't remove the existing ones.

// Step 2: Guard against protocol-relative open redirects on the raw pathname.
// normalizePath (step 4) would collapse //evil.com to /evil.com, causing the
// guard to miss it. Raw pathname must be checked first.
const protoGuard = guardProtocolRelativeUrl(url.pathname);
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.

The inline step comments (// Step 2: Guard against protocol-relative..., // Step 3: Strict segment-wise percent-decode..., etc.) were removed. These are valuable — they tie each code block to the numbered steps in the function-level doc block, making it easy to audit that the security-sensitive ordering hasn't drifted. The doc block steps were correctly renumbered; please keep the inline anchors too.

For example:

// Step 2: Guard against protocol-relative open redirects on the raw pathname.
const protoGuard = guardProtocolRelativeUrl(url.pathname);

const RSC_SEGMENT_SUFFIX = ".segment.rsc";

const SEGMENT_PREFETCH_PATTERN = new RegExp(
`^(/.*)${escapeRegex(RSC_SEGMENTS_DIR_SUFFIX)}(/.*)${escapeRegex(RSC_SEGMENT_SUFFIX)}$`,
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.

The first capture group (/.*) is greedy, which means for a path like /foo.segments/bar.segments/_tree.segment.rsc, the first group would capture /foo.segments/bar (consuming up to the last .segments). This is probably the correct behavior (the last .segments is the real protocol delimiter), but it's worth a brief comment explaining this is intentional, and ideally a test case for a path containing a literal .segments in the page portion.

Also — minor style nit: escapeRegex is called at module scope before its definition (line 24). This works because of hoisting, but reads oddly. Consider moving the helper above its first use.

Comment thread packages/vinext/src/entries/app-rsc-entry.ts
expect(
matchSegmentPrefetchRsc("/dashboard/settings.segments/tab/profile/__PAGE__.segment.rsc"),
).toBe(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.

Consider adding a test for percent-encoded .segments (e.g., /dashboard%2Esegments/_tree.segment.rsc). In the normalization pipeline, normalizePathnameForRouteMatchStrict decodes %2E to . before the segment-prefetch check runs, so this URL would match. That's probably fine (consistent with how .rsc suffix detection works post-decode), but it should be documented with a test.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review posted. Here's a summary of the feedback:

Overall: The normalizer and pipeline changes are solid. The URL pattern matching is correct, tests are thorough (22 unit + 7 integration), and the Phase 1 strategy of returning full-page RSC payloads is pragmatic.

Key issues raised:

  1. Double regex executionmatchSegmentPrefetchRsc + extractSegmentPrefetchRsc run the same regex twice on the hot path. Since extract already returns null for non-matches, the match guard is redundant. Suggested collapsing to a single call.

  2. Inline step comments removed — The // Step 2:, // Step 3: etc. comments in normalizeRscRequest were stripped. These are valuable for auditing security-sensitive ordering and should be kept.

  3. JSDoc field comments removed — The per-field doc comments on NormalizedRscRequest (e.g., /** Parsed URL. Callers may mutate... */) were removed. These are useful for IDE hover docs.

  4. segmentPrefetchPath threaded but unused — The value propagates through types and destructuring but is never consumed downstream. Fine for Phase 1 plumbing, but needs a comment so it doesn't look like dead code.

  5. Missing test for percent-encoded .segments%2Esegments would decode to .segments before the regex check. Should have a test documenting this behavior.

github run

…dflare#1335)

vinext currently has no endpoint for the Next.js 15+ RSC segment cache
prefetch protocol. The client-side router expects to make segment-level
prefetch requests (e.g. `/_tree`, `/dashboard/__PAGE__`) to URLs like
`/page.segments/_tree.segment.rsc` and receive segment-specific RSC
payloads. Without this endpoint, ~66 deploy suite tests time out waiting
for segment-prefetch requests that never fire.

This PR implements Phase 1 of the protocol — the server-side URL handling
and request routing. When a request arrives at `.segments/*.segment.rsc`,
the normalizer rewrites it back to the original page path and marks it as
a segment-prefetch RSC request. The page renders normally and returns a
valid RSC response that the client segment cache can process.

Implementation:
- New `app-segment-prefetch-normalizer.ts` — matches `.segments/*.segment.rsc`
  URLs and extracts the original page path + segment path
- Updated `normalizeRscRequest()` — detects segment-prefetch URLs in the
  normalization pipeline, rewrites the pathname back to the page path,
  sets `segmentPrefetchPath` on the normalized result
- Updated `app-rsc-handler.ts` — passes `segmentPrefetchPath` through
  `DispatchMatchedPageOptions` so downstream handlers can access it
- Updated `app-rsc-entry.ts` — accepts `segmentPrefetchPath` in the
  generated `dispatchMatchedPage` function signature
- No response format changes yet — segment-prefetch requests receive a
  full page RSC response (same as a regular `.rsc` prefetch), which the
  client's non-PPR code path handles gracefully

Why no `Next-Did-Postpone: 2` header: Without PPR, setting this header
would cause the client to attempt parsing the response as a
`RootTreePrefetch` structure. Returning the response without it lets the
client fall through to the NavigationFlightResponse code path, which
correctly handles the full-page RSC payload.

Trade-offs:
- This is intentionally minimal — the endpoint exists, responds with
  valid RSC content, and doesn't error. Full segment-level response
  generation (proper `RootTreePrefetch` / `SegmentPrefetchResponse`
  payloads) is deferred to a follow-up.
- Per-segment caching and the client-side segment cache scheduler are
  not implemented. The server returns the full page RSC for all segment
  prefetch requests, which is functionally correct but doesn't provide
  the granular caching benefits of the full protocol.
- Complex segment-cache E2E tests can be skipped with `adapter-api-e2e`
  annotations until Phase 2+ is complete.

Testing: 22 unit tests for the URL normalizer (match + extract), 7
integration tests for the request normalization pipeline. All existing
tests pass (251 across 6 core test files, 313 app-router, 292 features).
@yunus25jmi1 yunus25jmi1 force-pushed the feat/segment-cache-prefetch-protocol branch from b8961c6 to c0eb026 Compare May 22, 2026 09:59
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.

RSC segment cache prefetch protocol not implemented

2 participants