Skip to content

fix(app-router): preserve front redirects through fallback route handlers - #2000

Merged
james-elicx merged 19 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/front-redirect-issue
Jun 13, 2026
Merged

fix(app-router): preserve front redirects through fallback route handlers#2000
james-elicx merged 19 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/front-redirect-issue

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the App Router fallback rewrite + route-handler front redirect path covered by the upstream Next.js regression.

Changes:

  • dispatch fallback-rewritten RSC App route handlers with the userland-normalized request URL
  • match Next.js request URL behavior for _rsc: stripped for RSC and Edge App Route requests, preserved in ordinary Node App Route request.url, and always removed from parsed userland search params
  • prevent duplicate App Router bootstrap/hydration when the browser entry is evaluated under more than one module identity
  • close progressive RSC bootstrap streams when the done marker arrives without another chunk, and safely handle cancellation
  • port the upstream front-redirect-issue fixture with production E2E coverage for visible pathname and single bootstrap loading
  • run the regression project in the required CI E2E matrix

Upstream references

Verification

  • vp test run tests/app-browser-stream.test.ts tests/app-rsc-handler.test.ts
  • PLAYWRIGHT_PROJECT=app-front-redirect-issue vp run test:e2e
  • vp check
  • CI: Check, Vitest, and all Playwright E2E projects, including E2E (app-front-redirect-issue)

@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 1ed6a08

@NathanDrake2406
NathanDrake2406 force-pushed the nathan/front-redirect-issue branch from 5a94057 to 18b96a8 Compare June 13, 2026 08:57
@NathanDrake2406
NathanDrake2406 force-pushed the nathan/front-redirect-issue branch from 14ca1db to 8446db4 Compare June 13, 2026 11:24
@NathanDrake2406
NathanDrake2406 marked this pull request as ready for review June 13, 2026 11:26
@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: front-redirect-issue fix

Reviewed all 17 changed files plus surrounding context in app-rsc-handler.ts, app-rsc-request-normalization.ts, app-browser-entry.ts, and app-browser-stream.ts. Ran the unit suites:

  • vp test run tests/app-rsc-handler.test.ts -t "hides internal RSC cache-busting params" → 3 passed
  • vitest run tests/app-browser-stream.test.ts → 11 passed

(app-rsc-handler.test.ts only fails under a bare npx vitest because the repo's vp test runner is required; it passes under vp test run.)

What's correct

1. request: userlandRequest (app-rsc-handler.ts:773) — Correct and consistent. The route-handler dispatch was the only userland-facing dispatch still passing the raw request (with _rsc in request.url), while middleware, before/after-files rewrites already used userlandRequest. Note searchParams was already clean here because stripRscCacheBustingSearchParam(url) runs at line 598, so this change specifically aligns request.url with the already-stripped searchParams. The page dispatch correctly keeps the raw request (it needs the real request for RSC rendering). The fix flows through entries/app-rsc-entry.ts:785 to __dispatchAppRouteHandler, and since the App Router prod server delegates to the built RSC entry, dev/prod parity holds — no separate Pages-Router-style fix needed.

2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() correctly prevents a second module instance (loaded under a different ESM URL via deployment-id cache busting) from double-consuming the inline RSC and double-hydrating. The null-stream abort path leaving state at "starting" is acceptable because that branch triggers a full reload.

3. Progressive stream done-marker / cancel handling — The accessor defineProperty on done is configurable: true, so resetBrowserGlobals() deletion and stream re-creation both work. Microtask scheduling of closeOnce after a synchronous final-chunk push preserves ordering (verified by the existing replay test, which still passes).

Minor observations (non-blocking)

See inline comments.

}

async function main(): Promise<void> {
if (!claimInitialAppRouterBootstrap()) return;

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.

Minor: when readInitialRscStream() returns null (line 1582), __VINEXT_RSC_BOOTSTRAP_STATE__ stays "starting" permanently for this module instance. This is fine in practice because the null branch triggers a full reload (fresh module graph resets the marker), but it does mean a same-instance retry can't re-enter main(). Worth a one-line comment noting the reload-resets-the-marker invariant so a future refactor doesn't accidentally rely on re-entry.

}
},
cancel() {
cancelStream?.();

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.

After cancel(), the monkey-patched arr.push and the done accessor are left installed on the runtime/global object — cancelStream only flips closed. This is harmless (the if (closed) return guard and the no-op closeOnce neutralize late writes, and a subsequent createProgressiveRscStream() re-installs both since the accessor is configurable). Flagging only so it's a conscious choice rather than an oversight; no change required for the bootstrap-once scenario.

path: string;
}>;
},
): Promise<Response> {

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 handler re-fetches request.nextUrl after rewriting the pathname to /app-future/en/${path}. Confirmed this can't recurse through the fallback rewrite: the rewritten path matches the [lang]/(dashboard)/[teamSlug] page directly, so the fallback (which only fires when nothing else matches) doesn't re-trigger. Good — matches the upstream fixture intent.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's a summary of my findings:

Review Summary for PR #2000

The PR is well-implemented, correct, and well-tested. I verified the core mechanics by reading the full handler flow and running the affected unit suites (14 tests passing under the proper vp runner).

Key findings

The fix is correct:

  1. request: userlandRequest (app-rsc-handler.ts:773) — The route-handler dispatch was the last userland-facing dispatch still passing the raw request with _rsc in request.url. Middleware and before/after-files rewrites already used userlandRequest. This change aligns request.url with the already-stripped searchParams (stripped at line 598). Verified the fix flows through to __dispatchAppRouteHandler and that dev/prod parity holds (App Router prod delegates to the built RSC entry).

  2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() correctly prevents a second module instance (loaded under a different ESM URL via deployment-id cache busting) from double-hydrating.

  3. Progressive stream done/cancel handling — The configurable: true accessor allows proper cleanup and re-creation; microtask ordering preserves the final-chunk-before-close invariant.

Non-blocking observations (3 inline comments)

  • "starting" state never resets on the null-abort path (acceptable — reload resets it; suggested a clarifying comment)
  • Monkey-patched push/accessor remain installed after cancel() (harmless, re-installable)
  • Confirmed the fixture's re-fetch can't recurse through the fallback rewrite

One note: the PR description's verification commands reference files (deployment-id.ts, app-ssr-entry.ts) that aren't in this diff — that deployment-id work appears to have landed in a prior PR; this PR's actual scope is the 4 commits shown.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

1 similar comment
@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: front-redirect-issue fix

Reviewed all 17 files plus surrounding context in app-rsc-handler.ts, app-browser-entry.ts, app-browser-stream.ts, app-ssr-entry.ts, and base-path.ts. Ran the affected suites and checks locally:

  • vp test run tests/app-browser-stream.test.ts tests/app-rsc-handler.test.ts → 57 passed (11 + 46)
  • vp check on the 5 changed source/test files → format, lint, type checks clean
  • npx knip → exit 0 (validates the knip.ts change)

The fix is correct

1. request: userlandRequest (app-rsc-handler.ts:773) — This was the last userland-facing dispatch still passing the raw request (with _rsc in request.url). Middleware (:513), before/after-files (:545/:668), and fallback (:688) rewrites already use userlandRequest. searchParams was already clean here (stripped at :598), so this aligns request.url with the already-clean searchParams. The page dispatch correctly keeps the raw request. The fix flows through entries/app-rsc-entry.ts:785__dispatchAppRouteHandler, and since App Router prod delegates to the built RSC entry, dev/prod parity holds. The new unit test (tests/app-rsc-handler.test.ts:780) correctly exercises the route-handler branch and would fail without the fix.

2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() correctly gates main() so a second module instance (loaded under a deployment-id-busted ESM URL) can't double-consume the inline RSC or double-hydrate. Checking both __VINEXT_RSC_ROOT__ and __VINEXT_RSC_BOOTSTRAP_STATE__ covers the in-flight and committed cases.

3. latestHydrationPathAndSearch — Verified safe for basePath apps. The value comes from navContext.pathname (= canonicalPathname = basePath-stripped cleanPathname), and createClientNavigationRenderSnapshot re-applies stripBasePath, which is idempotent (utils/base-path.ts:22). So /docs/foo (old: window.location.href → strip → /foo) and /foo (new: already stripped → no-op) yield the same pathname. The meaningful change is the searchParams now reflect the server-rendered URL rather than the address bar — exactly what avoids the post-front-redirect hydration mismatch. It's only read once at bootstrap, so later navigations re-writing it is harmless.

4. Progressive stream done/cancel handling — The configurable: true accessor on done lets resetBrowserGlobals() deletion and stream re-creation work, and the microtask scheduling of closeOnce preserves the final-chunk-before-close ordering (the done marker is always a separate <script> from chunk pushes per build/prerender.ts:207). The cancel() path correctly flips closed and cancels the pending completion check.

Non-blocking observations

See inline comments. Nothing here blocks merge.

}
},
cancel() {
cancelStream?.();

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: after cancel() runs, the monkey-patched arr.push and the done accessor stay installed on the runtime/global — cancelStream only flips closed. This is harmless (the if (closed) return guard in push, and closeOnce/scheduleCloseOnce short-circuiting on closed, neutralize any late writes; and a subsequent createProgressiveRscStream() re-installs both since the accessor is configurable). Flagging only so the leak is a conscious choice rather than an oversight — no change required for the bootstrap-once production path.

// global assignment, even if a future refactor interposes async work here.
// The recovery path reloads the document, which resets the "starting" claim;
// this module instance is intentionally not eligible to retry bootstrap.
if (rscStream === null) return;

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: on the rscStream === null path, __VINEXT_RSC_BOOTSTRAP_STATE__ stays "starting" for this module instance, so a same-instance retry can't re-enter main(). The added comment (The recovery path reloads the document, which resets the "starting" claim) correctly documents why this is fine in practice. Good — this is the kind of invariant note that keeps a future refactor from accidentally relying on re-entry.

Comment thread playwright.config.ts Outdated
use: { baseURL: "http://localhost:4186" },
server: {
command:
"test -e node_modules || ln -s ../../../fixtures/app-basic/node_modules node_modules && npx vp run vinext#build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js start --port 4186",

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: test -e node_modules || ln -s ...test -e follows symlinks, so if a dangling symlink is left over from a prior local run (target deleted), test -e returns false and the subsequent ln -s fails with File exists. This won't affect CI (fresh checkout, reuseExistingServer: false), but for local reuse test -e || test -L (or ln -sf) would be more robust. Minor.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's my summary:

Review Summary for PR #2000

The PR is correct, well-scoped, and well-tested. I verified the full handler flow, ran the affected suites (57 tests passing), vp check (clean), and npx knip (exit 0).

Key findings — the fix is correct

  1. request: userlandRequest (app-rsc-handler.ts:773) — This was the last userland-facing dispatch still passing the raw request with _rsc in request.url. Now consistent with middleware and before/after/fallback rewrites. The new unit test correctly exercises the route-handler branch and would fail without the fix. Dev/prod parity holds since App Router prod delegates to the built RSC entry.

  2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() correctly prevents a second deployment-id-busted module instance from double-consuming the inline RSC or double-hydrating.

  3. latestHydrationPathAndSearch — I verified this is safe for basePath apps: navContext.pathname is already basePath-stripped, and createClientNavigationRenderSnapshot's stripBasePath is idempotent, so the pathname matches the old window.location.href behavior. The meaningful change is the searchParams now reflecting the server-rendered URL — exactly what fixes the post-front-redirect hydration mismatch.

  4. Progressive stream done/cancel — The configurable: true done accessor correctly closes the stream when the done marker arrives without a following chunk, and the microtask scheduling preserves final-chunk-before-close ordering.

Non-blocking observations (3 inline comments)

  • cancel() leaves the push/done patches installed (harmless, re-installable)
  • The "starting" claim never resets on the null-abort path (correctly documented as reload-resets)
  • The e2e test -e node_modules guard is fragile against dangling symlinks for local reuse (CI unaffected)

The knip.ts cleanup (removing the now-unneeded "agent" ignore) is benign — knip passes. The deployment-id work referenced in the PR description (built-asset-url.ts) is pre-existing, not in this diff, matching the prior reviewer's observation.

github run

@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: front-redirect-issue fix

Reviewed the full net diff against main (not just individual commits), read the request lifecycle in app-rsc-handler.ts, the bootstrap path in app-browser-entry.ts, and the stream lifecycle in app-browser-stream.ts. Ran the affected suites and checks locally:

  • vp test run tests/app-browser-stream.test.ts tests/app-rsc-handler.test.ts → 58 passed (11 + 47)
  • vp check on the 4 changed source files → format, lint, type checks clean
  • npx knip → exit 0

The three real fixes are correct and well-tested

  1. request: userlandRequest + always-strip _rsc (app-rsc-handler.ts:495, :595, :769) — Dropping the isRscRequest gate is safe: requestWithoutRscCacheBustingSearchParam early-returns the original request when no _rsc pair is present (:378), so ordinary requests are untouched (same object, no rebuild). The only behavioral change is that a request carrying a literal _rsc query now has it stripped for userland route handlers, which matches Next.js' stripInternalSearchParams. The route-handler dispatch was the last userland-facing dispatch still passing the raw request; this aligns request.url with the already-stripped searchParams. The page dispatch correctly keeps the raw request. New unit tests exercise both the fallback-rewritten and non-RSC branches.

  2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() gates main() on both __VINEXT_RSC_ROOT__ and __VINEXT_RSC_BOOTSTRAP_STATE__, correctly preventing a second deployment-id-busted module instance from double-consuming the inline RSC or double-hydrating.

  3. Progressive stream done/cancel handling — The configurable: true done accessor closes the stream when the done marker arrives without a following chunk, and the microtask scheduling preserves the final-chunk-before-close ordering (verified by the still-passing replay test, where setting done=true then push("final") keeps "final" readable because the synchronous push enqueues before the scheduled closeOnce runs).

Main finding: the PR description is stale and overstates the diff

The PR was reworked in-place. Two of the described changes were added and then reverted within the same PR, so the final net diff no longer matches the description:

  • "preserve the server-rendered hydration path/search when bootstrapping the browser router after an internal front redirect" — the latestHydrationPathAndSearch mechanism this referred to was removed in 4f6fefa (bootstrap is back to window.location.href). What survives in the net diff is a no-op local-variable extraction in restoreHydrationNavigationContext (see inline comment). The ?tab=latest query-preservation assertion in the e2e was also reverted (e25a2a0), so query preservation across a front redirect is no longer covered by this PR at all.
  • "avoid appending the deployment-id query to the native ESM bootstrap module URL" — no built-asset-url.ts/deployment-id source change is in this diff; the e2e dpl assertion relies on pre-existing behavior. The stripDeploymentIdQuery export was added in 191a9f1 and removed in 7823379, netting to zero.

None of this blocks merge — the surviving fixes are sound — but the description should be trimmed to the three changes that actually landed, and it's worth a maintainer's call whether the now-vestigial query-preservation work should be re-added (with coverage) or dropped cleanly.

Non-blocking observations

See inline comments. The knip.ts "agent" cleanup is benign (knip passes); it's unrelated to this PR's scope but harmless.

searchParams: SearchParamInput,
params: Record<string, string | string[]>,
): void {
const normalizedSearchParams = new URLSearchParams(searchParams);

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.

This normalizedSearchParams extraction is now a pure no-op refactor against main. It was introduced to feed latestHydrationPathAndSearch, but that mechanism was reverted in 4f6fefa (bootstrapHydration is back to window.location.href). The only remaining use is the immediate setNavigationContext call below, so the intermediate adds nothing over the previous inline new URLSearchParams(searchParams). Consider reverting to keep the diff minimal:

Suggested change
const normalizedSearchParams = new URLSearchParams(searchParams);
setNavigationContext({
pathname,
searchParams: new URLSearchParams(searchParams),
params,
});


await expect(page.locator("#home-page h1")).toHaveText("Hello!", { timeout: 10_000 });
expect(page.url()).toBe(`${BASE}/vercel-user`);
await expect(page.locator("#visible-url")).toHaveText("/vercel-user");

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.

With the ?tab=latest case reverted (e25a2a0), this only asserts the canonical pathname survives the front redirect — which window.location.href already provided before this PR. That's still a meaningful regression guard (it confirms the visible URL stays /vercel-user rather than the rewrite target /app-future/en/vercel-user), but it no longer exercises query-string preservation. If preserving the search across a front redirect is a goal, it'd be worth re-adding a query-param assertion here so the behavior is actually pinned; otherwise the VisibleUrl fixture's useSearchParams usage is unexercised.

}
},
cancel() {
cancelStream?.();

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 (matches the prior reviewer's note): after cancel() runs, the monkey-patched arr.push and the done accessor stay installed on the runtime/global — cancelStream only flips closed. Harmless because the if (closed) return guard in push and closeOnce/scheduleCloseOnce short-circuit on closed, and a later createProgressiveRscStream() re-installs both (the accessor is configurable). Flagging only so the leak is a conscious choice for the bootstrap-once production path.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's a summary of my findings.

Review Summary for PR #2000

I reviewed the full net diff against main (rather than just individual commits, which matters here because this PR was reworked in-place). I read the request lifecycle in app-rsc-handler.ts, the bootstrap path in app-browser-entry.ts, and the stream lifecycle in app-browser-stream.ts. Verified locally: 58 unit tests pass, vp check clean, npx knip exit 0.

The three real fixes are correct and well-tested

  1. Always-strip _rsc + request: userlandRequest — safe (early-returns original request when no _rsc present), aligns route-handler request.url with the already-stripped searchParams, matches Next.js' stripInternalSearchParams. Page dispatch correctly keeps the raw request.
  2. Bootstrap ownership guard — correctly prevents double-hydration from a second deployment-id-busted module instance.
  3. Progressive stream done/cancel — the configurable done accessor + microtask scheduling preserves final-chunk-before-close ordering.

Main finding: the PR description is stale and overstates the diff

The PR went back-and-forth in-place. Two described changes were added then reverted within the same PR:

  • The "preserve server-rendered hydration path/search" mechanism (latestHydrationPathAndSearch) was removed; bootstrap is back to window.location.href. The net residue is a no-op normalizedSearchParams extraction, and the ?tab=latest query-preservation e2e assertion was reverted, so query preservation is no longer covered at all.
  • The "deployment-id bootstrap URL" change isn't in this diff (stripDeploymentIdQuery was added in one commit, removed in another — net zero); the e2e dpl assertion relies on pre-existing behavior.

This doesn't block merge — the surviving fixes are sound — but the description should be trimmed, and a maintainer should decide whether the vestigial query-preservation work should be re-added with coverage or dropped cleanly.

I posted 3 inline comments (no-op extraction suggestion, the now-narrowed e2e assertion, and the cancel()-leaves-patches-installed note).

github run

@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: front-redirect-issue fix

Reviewed the full net diff against main and the surrounding request lifecycle in app-rsc-handler.ts, the bootstrap path in app-browser-entry.ts, and the stream lifecycle in app-browser-stream.ts. Verified locally:

  • vp test run tests/app-rsc-handler.test.ts tests/app-browser-stream.test.ts -> 58 passed (47 + 11)
  • vp check on the 4 changed source files -> format / lint / type clean
  • npx knip -> exit 0

The three real fixes are correct and well-tested

  1. request: userlandRequest + unconditional _rsc strip (app-rsc-handler.ts:495, :595, :769) — Dropping the isRscRequest gate is safe: requestWithoutRscCacheBustingSearchParam early-returns the original request object when no _rsc pair is present (:378), so ordinary requests are untouched. The route-handler dispatch was the last userland-facing dispatch still passing the raw request (middleware :511, before/after/fallback rewrites :543/:684 already used userlandRequest), so request.url is now aligned with the already-stripped searchParams, matching Next.js' stripInternalSearchParams. The page dispatch correctly keeps the raw request because page components read the stripped url.searchParams, not request.url. Both branches are covered by new unit tests.

  2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() gates main() on both __VINEXT_RSC_ROOT__ and __VINEXT_RSC_BOOTSTRAP_STATE__, correctly preventing a second deployment-id-busted module instance from double-consuming the inline RSC or double-hydrating. HMR is unaffected (it dispatches via rsc:update, never re-invokes main()).

  3. Progressive stream done/cancel — The configurable done accessor closes the stream when the done marker arrives without a following chunk, and microtask scheduling preserves final-chunk-before-close ordering. The inline <expr>.done=true marker (app-ssr-stream.ts:57, build/prerender.ts:213) is exactly what the setter intercepts. The cancel() path correctly flips closed and removes the DOMContentLoaded listener.

Note on prior review comments

The earlier automated review comments referencing a normalizedSearchParams no-op extraction and a latestHydrationPathAndSearch mechanism describe code that is not present in this final diffapp-browser-entry.ts is purely additive (the bootstrap guard) and bootstrapHydration still uses window.location.href unchanged. Likewise visible-url.tsx uses usePathname, not useSearchParams. Those earlier comments are themselves stale; nothing to action there. The PR description is still broader than the landed diff, but the surviving changes are sound.

No blocking issues. Two minor observations inline.

Comment thread playwright.config.ts Outdated
use: { baseURL: "http://localhost:4186" },
server: {
command:
"test -e node_modules || test -L node_modules || ln -s ../../../fixtures/app-basic/node_modules node_modules && npx vp run vinext#build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js start --port 4186",

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.

Minor (non-blocking): this command relies on &&/|| being left-associative and equal-precedence, so it happens to work in both branches (test -e || test -L || ln -s always evaluates truthy before the && chain). But it reads as if the ln -s is conditionally chained to the build, which is confusing. Wrapping the symlink setup in a group would make the intent explicit and avoid a future edit accidentally breaking precedence:

Suggested change
"test -e node_modules || test -L node_modules || ln -s ../../../fixtures/app-basic/node_modules node_modules && npx vp run vinext#build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js start --port 4186",
"{ test -e node_modules || test -L node_modules || ln -s ../../../fixtures/app-basic/node_modules node_modules; } && npx vp run vinext#build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js build && NEXT_DEPLOYMENT_ID=vinext-front-redirect-e2e node ../../../../packages/vinext/dist/cli.js start --port 4186",

(Also note test -e follows symlinks, so a dangling symlink left from a prior local run makes both test -e and test -L... actually test -L catches the dangling case, so this is fine for local reuse. CI is unaffected with a fresh checkout.)

}
},
cancel() {
cancelStream?.();

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: after cancel() runs, the monkey-patched arr.push and the redefined done accessor remain installed on the runtime/global — cancelStream only flips closed. This is harmless (the if (closed) return guard in push and closeOnce/scheduleCloseOnce short-circuit on closed, and a later createProgressiveRscStream() re-installs both since the accessor is configurable), and the new cancel test confirms late writes don't throw. Flagging only so the leftover patches are a conscious choice for the bootstrap-once production path rather than an oversight.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's my summary:

Review Summary for PR #2000

I reviewed the full net diff against main, read the request lifecycle in app-rsc-handler.ts, the bootstrap path in app-browser-entry.ts, and the stream lifecycle in app-browser-stream.ts. Verified locally: 58 unit tests pass, vp check clean, npx knip exit 0.

The three real fixes are correct and well-tested

  1. request: userlandRequest + unconditional _rsc strip — Safe (early-returns original request when no _rsc present), aligns the route-handler request.url with the already-stripped searchParams, matches Next.js' stripInternalSearchParams. Page dispatch correctly keeps the raw request since page components read url.searchParams, not request.url. Both branches covered by new unit tests.
  2. Bootstrap ownership guard — Correctly prevents double-hydration from a second deployment-id-busted module instance; HMR unaffected (dispatches via rsc:update, never re-invokes main()).
  3. Progressive stream done/cancel — The configurable done accessor + microtask scheduling preserves final-chunk-before-close ordering; cancel() correctly removes the DOMContentLoaded listener.

Key correction to prior automated reviews

I verified that the earlier bonk comments about a normalizedSearchParams no-op extraction, a latestHydrationPathAndSearch mechanism, and a useSearchParams fixture are stale — none of that code exists in this final diff. app-browser-entry.ts is purely additive, bootstrapHydration still uses window.location.href, and visible-url.tsx uses usePathname. I called that out so reviewers don't chase non-existent issues.

Non-blocking observations (2 inline)

  • The playwright server command relies on &&/|| precedence quirks — works but is fragile/confusing; suggested grouping the symlink setup.
  • cancel() leaves the push/done patches installed (harmless, re-installable; confirmed by the new cancel test).

No blocking issues — the PR is sound and ready to merge once CI is green.

github run

@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: front-redirect-issue fix

Reviewed the full net diff against main: the request lifecycle in app-rsc-handler.ts, the bootstrap claim in app-browser-entry.ts, the stream done/cancel logic in app-browser-stream.ts, plus the e2e fixture and playwright/CI wiring.

The three runtime fixes are correct

  1. request: userlandRequest + unconditional _rsc strip (app-rsc-handler.ts:495, :595, :769) — Dropping the isRscRequest gate is safe: requestWithoutRscCacheBustingSearchParam early-returns the original request object when no _rsc pair is present (:378), so ordinary requests are untouched (same object, no rebuild). The route-handler dispatch was the last userland-facing dispatch still passing the raw request; this aligns request.url with the already-stripped searchParams. Page dispatch correctly keeps the raw request. Both branches are covered by the new unit tests, and dev/prod parity holds since App Router prod delegates to the built RSC entry.

  2. Bootstrap ownership guardclaimInitialAppRouterBootstrap() gates main() on both __VINEXT_RSC_ROOT__ and __VINEXT_RSC_BOOTSTRAP_STATE__, correctly preventing a second deployment-id-busted module instance from double-consuming the inline RSC or double-hydrating. main() is only invoked once at module eval (:2302); HMR dispatches via rsc:update and never re-invokes it, so HMR is unaffected. This also closes the only path that could re-run createProgressiveRscStream() and orphan the first stream's done accessor — good.

  3. Progressive stream done/cancel — The configurable done accessor closes the stream when the done marker arrives without a following chunk. Because the done marker is always a separate <script> from chunk pushes (build/prerender.ts:207/:213), and the push patch enqueues synchronously while closeOnce is deferred via microtask, the final-chunk-before-close ordering is preserved. The cancel() path flips closed and cancels the pending completion check; later done=true/push are correctly neutralized by the if (closed) return / if (!closed) guards (verified by the new cancel test).

Note: the unconditional _rsc strip is a deliberate behavioral change

isRscCacheBustingSearchPair (app-rsc-cache-busting.ts:220) matches any param named _rsc regardless of value — not just one matching the computed cache-busting hash. So after this change, a non-RSC request carrying a literal ?_rsc=anything will have it stripped before userland route handlers see it. This matches Next.js, which treats _rsc as a reserved internal param and strips it unconditionally in stripInternalSearchParams, so it's correct parity. The new hides internal RSC cache-busting params from non-RSC route handler requests test pins exactly this. Worth keeping in mind that a user route handler can no longer observe a literal _rsc query param — that's intended.

Non-blocking observations

The prior automated reviews already flagged the cancel()-leaves-patches-installed note, the playwright &&/|| precedence, and the "starting" claim not resetting on the null-abort path. I agree those are all harmless. Two small additions inline.

No blocking issues — the PR is sound.

expect(page.url()).toBe(`${BASE}/vercel-user`);
await expect(page.locator("#visible-url")).toHaveText("/vercel-user");

const bootstrapSrc = await page.locator('script[type="module"][src]').getAttribute("src");

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.

page.locator('script[type="module"][src]') matches the first module script with a src. If the bootstrap output ever emits more than one such script (e.g. a vendor/preamble chunk before the entry), .getAttribute("src") silently resolves against whichever happens to be first in the DOM, which may not be the bootstrap entry. Since the assertion that follows (bootstrapRequests has length 1) is the real regression guard for single-hydration, this is low risk — but if multiple module scripts are emitted, consider .first() explicitly or filtering to the known bootstrap entry name so the test fails loudly rather than asserting against the wrong script.


export default async function TeamDashboardPage(props: { params: Promise<{ teamSlug: string }> }) {
const params = await props.params;
await new Promise((resolve) => setTimeout(resolve, 1000));

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 fixed setTimeout(resolve, 1000) (ported from upstream to simulate slow data) adds a hard 1s to every run of this project. It's within the 10s locator timeout in the spec, so it's fine, but it does make this the slowest single-test e2e project. If CI time matters, a shorter delay (e.g. 200ms) would still reproduce the streamed-redirect timing window the regression depends on. Non-blocking.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's my summary:

Review Summary for PR #2000

I reviewed the full net diff against main and traced the request lifecycle (app-rsc-handler.ts), the bootstrap claim (app-browser-entry.ts), and the stream done/cancel logic (app-browser-stream.ts), plus the e2e fixture, playwright, and CI wiring.

The three runtime fixes are correct

  1. request: userlandRequest + unconditional _rsc strip — Safe: requestWithoutRscCacheBustingSearchParam early-returns the original request when no _rsc is present (:378), so ordinary requests are untouched. The route-handler dispatch was the last userland-facing one still passing raw request; now aligned with the stripped searchParams. Page dispatch correctly keeps raw request. Both branches covered by new unit tests; dev/prod parity holds.

  2. Bootstrap ownership guard — Correctly prevents a second deployment-id-busted module instance from double-hydrating. main() runs once at module eval; HMR uses rsc:update and never re-invokes it. This also closes the only path that could re-run createProgressiveRscStream() and orphan the first stream's done accessor.

  3. Progressive stream done/cancel — The configurable done accessor + microtask-deferred closeOnce preserves final-chunk-before-close ordering (done marker is always a separate <script>). cancel() correctly neutralizes late writes via the closed guards.

One behavioral note I made explicit

The _rsc matcher (isRscCacheBustingSearchPair) strips any param named _rsc regardless of value — so a non-RSC request with a literal ?_rsc=foo now has it stripped from userland route handlers. This is correct Next.js parity (_rsc is a reserved internal param, unconditionally stripped in stripInternalSearchParams), and the new non-RSC test pins it — but it's a deliberate behavior change worth documenting.

Non-blocking inline comments (2)

  • The e2e script[type="module"][src] selector could match the wrong script if multiple module scripts are emitted; suggested making it explicit.
  • The fixture's hard 1s setTimeout makes this the slowest e2e project; a shorter delay would still reproduce the timing window.

No blocking issues — the PR is sound and well-tested. The prior reviews' non-blocking notes (cancel() leaves patches, playwright precedence, the "starting" null-abort path) are all accurate and harmless.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

2 similar comments
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@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: front-redirect-issue fix

Reviewed the full net diff against main (15 commits — the PR was reworked in-place, so individual commits don't reflect the landed state). Verified locally: vp test run tests/app-rsc-handler.test.ts tests/app-browser-stream.test.ts → 59 passed (48 + 11); vp check on the 3 changed source files → clean.

The key thing I want to flag for human reviewers: the route-handler dispatch logic in the final two commits (0251e10, c99f2df) is materially different from what every prior automated review described. The earlier reviews kept saying "request: userlandRequest — strips _rsc for all route handlers." That is no longer what the code does. The landed code at app-rsc-handler.ts:766 is:

const routeHandlerRequest =
  isRscRequest || isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;

So there are now three cases:

  • RSC request → stripped URL (userlandRequest)
  • Edge route handler (export const runtime = "edge") → stripped URL
  • Ordinary (non-RSC, node) route handler → raw request with _rsc preserved in request.url

I independently checked this against Next.js source (since the prior reviews predate this design):

  • Edge handlers go through web/adapter.ts, which builds the NextRequest from stripInternalSearchParams(normalizeURL) — so edge route handlers see _rsc stripped from the URL. ✅ matches isEdgeRouteHandleruserlandRequest.
  • Node handlers: base-server.ts / next-server.ts only delete query[NEXT_RSC_UNION_QUERY] on the parsed query object, not on request.url (which is rebuilt from initURL and keeps _rsc). So node route handlers see _rsc preserved in request.url. ✅ matches the : request fallback.

This is a genuinely subtle parity detail and the implementation gets it right. isEdgeRouteHandler reading handler.runtime is also consistent with how vinext resolves runtime elsewhere (app-segment-config.ts:126 reads segment.runtime off the module exports). The three new unit tests pin all three branches.

Net residue confirmed clean: latestHydrationPathAndSearch, normalizedSearchParams, and stripDeploymentIdQuery (flagged by earlier bot runs) do not exist in the final diff — bootstrapHydration still uses window.location.href and app-browser-entry.ts is purely additive. Those earlier comments are stale.

The bootstrap ownership guard and the progressive-stream done/cancel handling are both correct and well-tested.

One real concern

The PR description's "Verification" section and several described changes (deployment-id bootstrap URL, hydration path/search preservation) describe work that is not in this diff. Combined with the fact that the route-handler design changed twice after the description was written, the description materially overstates and misdescribes the landed change. A maintainer should trim it to the changes that actually shipped, and confirm the edge/node split was an intentional design decision (it's correct, but it's worth recording why node preserves _rsc while edge strips it, since it looks backwards at first glance).

No blocking issues — the surviving fixes are sound. Two minor non-blocking notes inline.

buildPageCacheTags(cleanPathname, [], [...route.routeSegments], "route"),
);
const routeHandlerRequest =
isRscRequest || isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;

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 edge-vs-node _rsc split here is correct Next.js parity but extremely non-obvious — at a glance it reads backwards (why would edge strip and node preserve?). The reason: edge route handlers run through web/adapter.ts, which builds the request from stripInternalSearchParams(normalizeURL) (URL stripped), whereas node handlers in base-server.ts/next-server.ts only delete query[NEXT_RSC_UNION_QUERY] on the parsed query object and rebuild request.url from initURL (which keeps _rsc).

Worth a comment so a future refactor doesn't "simplify" this back to unconditional stripping (which is exactly what every prior review of this PR assumed the code did):

Suggested change
isRscRequest || isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;
// Next.js parity: edge route handlers go through web/adapter.ts which
// strips internal search params from the request URL
// (stripInternalSearchParams), so they see `_rsc` removed. Node route
// handlers only strip `_rsc` from the parsed query object — request.url is
// rebuilt from initURL and keeps `_rsc`. RSC requests always use the
// stripped userland URL. Do not collapse this to unconditional stripping.
const routeHandlerRequest =
isRscRequest || isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;


const bootstrapScripts = page.locator('script[type="module"][src]');
await expect(bootstrapScripts).toHaveCount(1);
const bootstrapSrc = await bootstrapScripts.getAttribute("src");

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: bootstrapScripts.getAttribute("src") will throw a Playwright strict-mode violation if the locator ever resolves to more than one element. The toHaveCount(1) assertion on the line above guards the happy path (so this is safe today), but if a vendor/preamble module script is ever emitted before the entry, line 24 fails first with a count mismatch rather than this line silently reading the wrong src. Consider .first() or filtering to the known bootstrap entry name to make the intent explicit and the failure mode loud.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's my summary:

Review Summary for PR #2000

I reviewed the full net diff against main and ran the affected suites locally (59 tests pass, vp check clean).

Key new finding the prior automated reviews all missed

Every prior bot review described the route-handler fix as "request: userlandRequest — strips _rsc for all route handlers." That is no longer what the code does. The last two commits (0251e10, c99f2df) reworked it into a three-way split at app-rsc-handler.ts:766:

const routeHandlerRequest =
  isRscRequest || isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;
  • RSC request → stripped URL
  • Edge route handler → stripped URL
  • Ordinary node route handler → raw request with _rsc preserved

I independently verified this against Next.js source (which the prior reviews predated):

  • Edge handlers run through web/adapter.ts, which builds the request from stripInternalSearchParams(normalizeURL) → URL stripped ✅
  • Node handlers only delete query[NEXT_RSC_UNION_QUERY] on the parsed query object; request.url is rebuilt from initURL and keeps _rsc → URL preserved ✅

So the split is correct Next.js parity, even though it reads backwards at first glance. isEdgeRouteHandler reading handler.runtime is consistent with how vinext resolves runtime elsewhere. All three branches are pinned by new unit tests.

Verified clean

The latestHydrationPathAndSearch / normalizedSearchParams / stripDeploymentIdQuery items flagged by earlier bot runs do not exist in the final diff — those comments were reviewing intermediate commits and are stale.

One real concern (non-blocking)

The PR description overstates/misdescribes the landed change (references deployment-id and hydration-path work not in this diff). A maintainer should trim it and record why the edge/node _rsc split is intentional, since it's the kind of thing a future refactor would wrongly "simplify."

I posted 2 inline notes: a suggested explanatory comment on the edge/node split, and a Playwright strict-mode .first() note on the spec. No blocking issues.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

2 similar comments
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@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: front-redirect-issue fix

Reviewed the full net diff against main (the PR was reworked across 19 commits, so the landed code differs materially from what earlier automated reviews described — those reviews predate the final requestWithoutRscSuffix / external-rewrite rework). I traced the request lifecycle in app-rsc-handler.ts, the bootstrap claim in app-browser-entry.ts, and the stream done/cancel logic in app-browser-stream.ts, and verified the edge/node _rsc semantics against the Next.js canary source. Verified locally: vp test run tests/app-rsc-handler.test.ts tests/app-browser-stream.test.ts -> 60 passed; vp check on the 4 changed source files -> clean; npx knip -> exit 0.

The fixes are correct and well-tested

1. normalizedUserlandRequest vs userlandRequest split (app-rsc-handler.ts:510-511)userlandRequest now unconditionally strips _rsc (the isRscRequest gate was dropped). requestWithoutRscCacheBustingSearchParam early-returns the original object when no _rsc pair is present (:383), so ordinary requests are untouched. This matches Next.js' stripInternalSearchParams (internal-utils.ts), which strips _rsc unconditionally. Middleware and metadata correctly continue to use the stripped userlandRequest.

2. External rewrite _rsc forwarding is a deliberate behavior reversal — and it's correct parity. The before/after/fallback rewrites changed from request: userlandRequest (stripped) to request: normalizedUserlandRequest (preserves _rsc). Since applyRewrite only uses request for external rewrites (proxyExternalRequest, :333), this only affects full-URL proxy destinations. I verified against Next.js: resolve-routes.ts never strips _rsc before the external proxy path, and proxy-request.ts forwards parsedUrl.query verbatim — so Next.js does forward _rsc to external rewrite destinations. The prior vinext behavior (stripping it) was the divergence. The test flip from .toBe(false) to .toBe(true) is the right call. This is the most consequential change in the PR and is barely mentioned in the description — worth calling out explicitly so it isn't mistaken for an accidental regression of the prior PR's tested behavior.

3. Edge vs Node route-handler split (:779) — Verified against Next.js canary: edge handlers go through web/adapter.ts:185 which builds the request from stripInternalSearchParams(normalizeURL) (URL stripped), while node handlers only delete query[NEXT_RSC_UNION_QUERY] on the parsed query and rebuild request.url from initURL (preserved). The isEdgeRouteHandler ? userlandRequest : normalizedUserlandRequest split is exactly correct, and the inline comment documents the non-obvious reasoning. All three branches are pinned by new unit tests.

4. Bootstrap ownership guardclaimInitialAppRouterBootstrap() correctly prevents a second deployment-id-busted module instance from double-consuming the inline RSC or double-hydrating. The null-abort-leaves-"starting" path is documented as reload-resets.

5. Progressive stream done/cancel — The configurable done accessor + microtask-deferred closeOnce preserves final-chunk-before-close ordering; cancel() correctly neutralizes late writes.

Non-blocking observations

  • The PR description references work (deployment-id bootstrap URL, hydration path/search preservation) that is not in this diffbuilt-asset-url.ts/deployment-id.ts are pre-existing and unchanged, and bootstrapHydration still uses window.location.href. The description should be trimmed to the changes that actually landed, and should foreground the external-rewrite _rsc reversal (#2 above).
  • The knip.ts removal of the "agent" ignore is unrelated to this PR's scope but benign — knip still passes (the "agent" vitest reporter in vite.config.ts:109 is a reporter-name string, not a resolved dependency).

No blocking issues — the surviving fixes are sound, well-tested, and correctly match Next.js behavior.

@@ -492,9 +507,8 @@ async function handleAppRscRequest<TRoute extends AppRscHandlerRoute>(
// Keep cache-busting validation on the real request above, then hide the
// internal `_rsc` transport query from userland middleware and post-middleware
// has/missing matching. This mirrors Next.js' navigation middleware fixture.

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.

Minor: this comment now only describes the userlandRequest (middleware/has-missing) purpose, but the line below it introduces a second derived request (normalizedUserlandRequest) with the opposite _rsc policy that feeds the external rewrites and node route handlers. Since the external-rewrite _rsc forwarding (a reversal of the prior tested behavior at :557/:675/:694) is the most surprising change in this PR, a one-line note here distinguishing the two would help a future reader:

Suggested change
// has/missing matching. This mirrors Next.js' navigation middleware fixture.
// Keep cache-busting validation on the real request above, then derive two
// userland-facing request views:
// - `normalizedUserlandRequest`: `.rsc` suffix stripped, `_rsc` PRESERVED.
// Used for external rewrite proxies + Node route handlers (Next.js
// forwards `_rsc` to external rewrites and rebuilds Node request.url
// from initURL, keeping `_rsc`).
// - `userlandRequest`: also strips `_rsc`. Used for middleware,
// post-middleware has/missing matching, and edge route handlers
// (which run through web/adapter.ts -> stripInternalSearchParams).
// This mirrors Next.js' navigation middleware fixture.
const normalizedUserlandRequest = requestWithoutRscSuffix(request);
const userlandRequest = requestWithoutRscCacheBustingSearchParam(normalizedUserlandRequest);

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's a summary of my findings.

Review Summary for PR #2000

I reviewed the full net diff against main and verified the behavior against Next.js canary source (cloned locally). The landed code differs materially from what all prior automated reviews described — the PR was reworked across 19 commits, and those reviews predate the final requestWithoutRscSuffix / external-rewrite rework. Verified locally: 60 unit tests pass, vp check clean, npx knip exit 0.

The fixes are correct and well-tested

  1. normalizedUserlandRequest vs userlandRequest splituserlandRequest now unconditionally strips _rsc (matches Next.js stripInternalSearchParams), with a safe early-return for requests without _rsc.

  2. External rewrite _rsc forwarding — the most consequential change. This PR reverses a previously-tested behavior: before/after/fallback rewrites now forward _rsc to external proxy destinations instead of stripping it. I verified against Next.js source (resolve-routes.ts + proxy-request.ts) that Next.js does forward _rsc to external rewrites — so the prior vinext behavior was the divergence and this is correct parity. The test flip from .toBe(false) to .toBe(true) is right.

  3. Edge vs Node route-handler split — Verified against web/adapter.ts:185 (edge strips via stripInternalSearchParams) vs node base-server.ts (preserves _rsc in request.url). The isEdgeRouteHandler ? userlandRequest : normalizedUserlandRequest split is exactly correct.

  4. Bootstrap ownership guard and 5. progressive stream done/cancel — both correct and well-tested.

Non-blocking notes

  • The PR description references deployment-id and hydration-path work that is not in this diff — should be trimmed and should foreground the external-rewrite _rsc reversal.
  • The knip.ts "agent" cleanup is benign (knip passes).
  • Posted one inline suggestion to clarify the two-request comment, since the external-rewrite reversal is easy to mistake for an accidental regression.

No blocking issues — the fixes are sound and match Next.js behavior.

github run

@james-elicx
james-elicx merged commit d1a80cd into cloudflare:main Jun 13, 2026
44 checks passed
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