-
Notifications
You must be signed in to change notification settings - Fork 329
feat(app-router): implement RSC segment cache prefetch protocol #1420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
816e171
847ec47
6a6bc7b
c0eb026
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ import { | |||||||||||||||
| parseAppRscRenderMode, | ||||||||||||||||
| type AppRscRenderMode, | ||||||||||||||||
| } from "./app-rsc-render-mode.js"; | ||||||||||||||||
| import { extractSegmentPrefetchRsc } from "./app-segment-prefetch-normalizer.js"; | ||||||||||||||||
| import { badRequestResponse, notFoundResponse } from "./http-error-responses.js"; | ||||||||||||||||
|
|
||||||||||||||||
| export { normalizeMountedSlotsHeader } from "./app-mounted-slots-header.js"; | ||||||||||||||||
|
|
@@ -40,6 +41,17 @@ export type NormalizedRscRequest = { | |||||||||||||||
| renderMode: AppRscRenderMode; | ||||||||||||||||
| /** Disabled ClientReuseManifest hint. Never authorizes skip transport in this stage. */ | ||||||||||||||||
| clientReuseManifest: ClientReuseManifestParseResult; | ||||||||||||||||
| /** | ||||||||||||||||
| * When present, this is a segment-prefetch RSC request for a specific | ||||||||||||||||
| * route segment (e.g. "/_tree", "/dashboard/__PAGE__"). The cleanPathname | ||||||||||||||||
| * is the original page path after the .segments/ prefix was stripped. | ||||||||||||||||
| * | ||||||||||||||||
| * Currently threaded through DispatchMatchedPageOptions for Phase 1 plumbing. | ||||||||||||||||
| * Not yet consumed by render — the handler returns full-page RSC for all | ||||||||||||||||
| * segment prefetch requests until Phase 2 adds segment-level response | ||||||||||||||||
| * generation. | ||||||||||||||||
| */ | ||||||||||||||||
| segmentPrefetchPath: string | null; | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
|
|
@@ -59,14 +71,14 @@ export type NormalizedRscRequest = { | |||||||||||||||
| * 4. Collapse double-slashes, resolve `.` and `..` segments (normalizePath) | ||||||||||||||||
| * 5. basePath check + strip — 404 when pathname lacks the basePath prefix. | ||||||||||||||||
| * `/__vinext/` bypasses this for internal prerender endpoints. | ||||||||||||||||
| * 6. RSC detection: `.rsc` suffix only. RSC headers do not select payload | ||||||||||||||||
| * rendering at the canonical HTML URL, so caches that ignore Vary cannot | ||||||||||||||||
| * store Flight responses under HTML URLs. | ||||||||||||||||
| * 7. cleanPathname — pathname with `.rsc` suffix stripped | ||||||||||||||||
| * 8. Sanitize X-Vinext-Interception-Context — strip null bytes (header injection) | ||||||||||||||||
| * 9. Normalize x-vinext-mounted-slots — dedup and sort for canonical cache keys | ||||||||||||||||
| * 10. Read semantic render mode for refresh/action payload rendering | ||||||||||||||||
| * 11. Parse disabled ClientReuseManifest hints on canonical RSC payload requests | ||||||||||||||||
| * 6. Segment-prefetch detection: `.segments/*.segment.rsc` URLs are normalized back | ||||||||||||||||
| * to the original page path. The segment path is extracted for downstream handling. | ||||||||||||||||
| * 7. RSC detection: `.rsc` suffix only. Segment-prefetch requests are always treated as RSC. | ||||||||||||||||
| * 8. cleanPathname — pathname with `.rsc` suffix stripped | ||||||||||||||||
| * 9. Sanitize X-Vinext-Interception-Context — strip null bytes (header injection) | ||||||||||||||||
| * 10. Normalize x-vinext-mounted-slots — dedup and sort for canonical cache keys | ||||||||||||||||
| * 11. Read semantic render mode for refresh/action payload rendering | ||||||||||||||||
| * 12. Parse disabled ClientReuseManifest hints on canonical RSC payload requests | ||||||||||||||||
| * | ||||||||||||||||
| * @returns A 400 or 404 Response for invalid or out-of-scope inputs, | ||||||||||||||||
| * or a NormalizedRscRequest for valid requests. | ||||||||||||||||
|
|
@@ -107,19 +119,34 @@ export function normalizeRscRequest( | |||||||||||||||
| pathname = stripBasePath(pathname, basePath); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Steps 6-7: RSC detection and cleanPathname. | ||||||||||||||||
| const isRscRequest = pathname.endsWith(".rsc"); | ||||||||||||||||
| // Step 6: Segment-prefetch URL detection. | ||||||||||||||||
| // extractSegmentPrefetchRsc returns null for non-matching paths (single | ||||||||||||||||
| // regex run) so no separate match guard is needed. | ||||||||||||||||
| let segmentPrefetchPath: string | null = null; | ||||||||||||||||
| const extracted = extractSegmentPrefetchRsc(pathname); | ||||||||||||||||
| if (extracted) { | ||||||||||||||||
| segmentPrefetchPath = extracted.segmentPath; | ||||||||||||||||
| pathname = extracted.originalPathname; | ||||||||||||||||
| } | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||
|
|
||||||||||||||||
| // Steps 7-8: RSC detection and cleanPathname. | ||||||||||||||||
| // Segment-prefetch requests are always treated as RSC requests regardless | ||||||||||||||||
| // of whether the rewritten pathname ends with .rsc, because the original | ||||||||||||||||
| // URL had a .segment.rsc suffix. | ||||||||||||||||
| const isRscRequest = pathname.endsWith(".rsc") || segmentPrefetchPath !== null; | ||||||||||||||||
| const cleanPathname = stripRscSuffix(pathname); | ||||||||||||||||
|
|
||||||||||||||||
| // Step 8: Sanitize X-Vinext-Interception-Context. | ||||||||||||||||
| // Step 9: Sanitize X-Vinext-Interception-Context. | ||||||||||||||||
| // Null bytes in header values can be used for injection in some HTTP stacks. | ||||||||||||||||
| const interceptionContextHeader = | ||||||||||||||||
| request.headers.get(VINEXT_INTERCEPTION_CONTEXT_HEADER)?.replaceAll("\0", "") || null; | ||||||||||||||||
|
|
||||||||||||||||
| // Step 9: Normalize mounted-slots header for canonical cache keying. | ||||||||||||||||
| // Step 10: Normalize mounted-slots header for canonical cache keying. | ||||||||||||||||
| const mountedSlotsHeader = normalizeMountedSlotsHeader( | ||||||||||||||||
| request.headers.get(VINEXT_MOUNTED_SLOTS_HEADER), | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| // Step 11: Read semantic render mode for refresh/action payload rendering. | ||||||||||||||||
| const renderMode = isRscRequest | ||||||||||||||||
| ? parseAppRscRenderMode(request.headers.get(VINEXT_RSC_RENDER_MODE_HEADER)) | ||||||||||||||||
| : APP_RSC_RENDER_MODE_NAVIGATION; | ||||||||||||||||
|
|
@@ -136,5 +163,6 @@ export function normalizeRscRequest( | |||||||||||||||
| interceptionContextHeader, | ||||||||||||||||
| mountedSlotsHeader, | ||||||||||||||||
| renderMode, | ||||||||||||||||
| segmentPrefetchPath, | ||||||||||||||||
| }; | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /** | ||
| * Segment-prefetch RSC URL normalizer. | ||
| * | ||
| * Matches and extracts segment-prefetch URLs following the Next.js convention: | ||
| * /page-path.segments/_tree.segment.rsc | ||
| * /page-path.segments/_index.segment.rsc | ||
| * /page-path.segments/dashboard/__PAGE__.segment.rsc | ||
| * | ||
| * The normalizer extracts the original page pathname and the segment path, | ||
| * allowing the request to be routed through the normal RSC pipeline with | ||
| * the segment metadata set as request markers. | ||
| * | ||
| * Constants match Next.js: RSC_SEGMENTS_DIR_SUFFIX = '.segments', | ||
| * RSC_SEGMENT_SUFFIX = '.segment.rsc' | ||
| */ | ||
|
|
||
| const RSC_SEGMENTS_DIR_SUFFIX = ".segments"; | ||
| const RSC_SEGMENT_SUFFIX = ".segment.rsc"; | ||
|
|
||
| const SEGMENT_PREFETCH_PATTERN = new RegExp( | ||
| `^(/.*)${escapeRegex(RSC_SEGMENTS_DIR_SUFFIX)}(/.*)${escapeRegex(RSC_SEGMENT_SUFFIX)}$`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first capture group Also — minor style nit: |
||
| ); | ||
|
|
||
| function escapeRegex(str: string): string { | ||
| return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| } | ||
|
|
||
| type SegmentPrefetchResult = { | ||
| /** The original page pathname without segment-prefetch suffixes (e.g. "/dashboard"). */ | ||
| originalPathname: string; | ||
| /** The segment path (e.g. "/_tree", "/_index", "/dashboard/__PAGE__"). */ | ||
| segmentPath: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Check if a pathname matches the segment-prefetch URL pattern and extract the | ||
| * original page pathname and segment path. | ||
| * | ||
| * Returns null when the pathname does not match the expected pattern. | ||
| * This is the single entry point — prefer it over a separate match + extract | ||
| * to avoid running the regex twice on the hot path. | ||
| */ | ||
| export function extractSegmentPrefetchRsc(pathname: string): SegmentPrefetchResult | null { | ||
| const match = pathname.match(SEGMENT_PREFETCH_PATTERN); | ||
| if (!match) return null; | ||
| return { originalPathname: match[1], segmentPath: match[2] }; | ||
| } | ||
|
|
||
| /** | ||
| * Check if a pathname matches the segment-prefetch URL pattern. | ||
| * | ||
| * Prefer extractSegmentPrefetchRsc() when you need the extracted path info, | ||
| * since it runs the regex once and returns null for non-matches. | ||
| */ | ||
| export function matchSegmentPrefetchRsc(pathname: string): boolean { | ||
| return SEGMENT_PREFETCH_PATTERN.test(pathname); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.