Skip to content

fix(app-router): honor basePath false rewrites#2370

Open
james-elicx wants to merge 11 commits into
mainfrom
codex/fix-app-basepath-false-rewrites-28143992598
Open

fix(app-router): honor basePath false rewrites#2370
james-elicx wants to merge 11 commits into
mainfrom
codex/fix-app-basepath-false-rewrites-28143992598

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • allow App Router config rules with basePath: false to handle requests outside the configured mount
  • preserve the normal 404 boundary for unclaimed out-of-basePath App and hybrid Pages requests
  • hard-navigate same-origin Server Action redirects that leave the App Router basePath

Next.js parity

Ported from Next.js v16.2.6:

  • test/e2e/app-dir/app-basepath/index.test.ts

Fixes the run 28143992598 failure:

  • should redirect externally when encountering absolute URLs on the same host outside the basePath

Validation

  • vp test run tests/app-rsc-request-normalization.test.ts tests/app-rsc-handler.test.ts tests/app-browser-server-action-client.test.ts --maxWorkers=1 — 150 passed
  • targeted vp check on all touched files
  • vp run vinext#build
  • exact Next.js v16.2.6 deploy suite, one worker: test/e2e/app-dir/app-basepath/index.test.ts — 13/13 passed

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 6d9ecd5

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review: fix(app-router): honor basePath false rewrites (#2370)

I reviewed the change and ran a focused reproduction. The server-action client change and the basePath: false rewrite path both look correct, but the normalization change opens a parity regression for unclaimed out-of-basePath requests.

Blocking: image-optimization, metadata routes, and public files leak outside basePath

Before this PR, normalizeRscRequest returned a 404 for any out-of-basePath request, so nothing downstream ran. Now normalizeRscRequest(request, options.basePath, true) (app-rsc-handler.ts:494) lets those requests through, and the new outOfBasePathRequestClaimed gate (app-rsc-handler.ts:725) only guards preActionMatch/options.matchRoute and renderPagesForMatchKind.

Three handlers run before that gate and are therefore reachable by unclaimed out-of-basePath requests:

  • isImageOptimizationPath(cleanPathname)resolveDevImageRedirect (app-rsc-handler.ts:659)
  • options.handleMetadataRouteRequest(cleanPathname) (app-rsc-handler.ts:674)
  • resolvePublicFileRoute({ cleanPathname, ... }) (app-rsc-handler.ts:688)

With basePath: /docs, Next.js serves these only under the basePath (/docs/favicon.ico, /docs/_next/image, /docs/sitemap.xml); a request to the un-prefixed path must 404. The PR's own existing tests confirm the intended design — public/favicon are served at /docs/favicon.ico (tests/app-rsc-handler.test.ts:2092,2128). But the out-of-basePath case is now broken.

I verified this with a throwaway test against the actual handler (basePath /docs, publicFiles: new Set(["/favicon.ico"])):

  • GET /favicon.ico200 (expected 404)
  • GET /_next/image?url=%2Fimg.jpg&w=640&q=75302 (expected 404)

Metadata routes (sitemap.xml, etc.) have the same exposure since they're matched on the un-stripped cleanPathname before the gate.

Suggested fix: move the outOfBasePathRequestClaimed computation up and gate these three handlers too (an unclaimed out-of-basePath request must not be served by image/metadata/public-file handling). Note the ordering subtlety: beforeFiles rewrites run before these handlers and already flip resolvedUrl !== originalResolvedUrl, so a basePath: false beforeFiles rewrite that lands on a public file / metadata route would still be claimed and served correctly — only the truly unclaimed case should 404.

Please also add regression tests for unclaimed out-of-basePath /favicon.ico, /_next/image, and a metadata route (e.g. /sitemap.xml) asserting 404, mirroring the existing in-basePath tests.

Non-blocking: stale comment

app-rsc-handler.ts:518-523 still says:

The App Router does not yet support basePath: false rules — they would need a pre-strip hook in normalizeRscRequest to fire. Tracked as follow-up to issue #1333.

This PR adds exactly that support (see the new test at tests/app-rsc-handler.test.ts:146). The comment now contradicts the behavior — please update or remove it so it doesn't mislead future readers.

Looks good

  • resolveActionRedirectTarget correctly hard-navigates same-origin redirects that leave the basePath (app-browser-server-action-client.ts:96-102), and the new test exercises it well.
  • The renderPagesFallback / route-match gating via outOfBasePathRequestClaimed is correct, and the 404 boundary for hybrid Pages data requests is preserved (tests/app-rsc-handler.test.ts:1823).
  • redirectDestinationWithBasePath correctly skips the basePath prefix when !hadBasePath.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Posted the review to PR #2370. Summary of findings:

Review summary

Blocking issue — out-of-basePath leak (verified empirically):

The change normalizeRscRequest(request, options.basePath, true) now lets out-of-basePath requests through to the handler, but the new outOfBasePathRequestClaimed gate (line 725) only guards route matching and renderPagesFallback. Three handlers run before that gate and are reachable by unclaimed out-of-basePath requests:

  • Image optimization (app-rsc-handler.ts:659)
  • Metadata routes (app-rsc-handler.ts:674)
  • Public files (app-rsc-handler.ts:688)

I reproduced this against the real handler with basePath /docs:

  • GET /favicon.ico200 (Next.js parity: should be 404)
  • GET /_next/image?...302 (should be 404)

In Next.js these are served only under the basePath, so unclaimed out-of-basePath requests must 404. Suggested fix: hoist the outOfBasePathRequestClaimed computation and gate those three handlers, preserving the case where a beforeFiles basePath: false rewrite legitimately claims the request.

Non-blocking: The comment at app-rsc-handler.ts:518-523 ("App Router does not yet support basePath: false rules") now contradicts what this PR ships and should be updated.

Looks good: the server-action redirect hard-navigation, the renderPagesFallback gating, the preserved 404 boundary, and redirectDestinationWithBasePath handling of !hadBasePath.

github run

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 6d9ecd5 against base de014d4 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 117.1 KB 117.1 KB ⚫ -0.0%
Client entry size (gzip) vinext 112.5 KB 112.5 KB ⚫ -0.0%
Dev server cold start vinext 2.63 s 2.62 s ⚫ -0.2%
Production build time vinext 3.17 s 3.16 s ⚫ -0.3%
RSC entry closure size (gzip) vinext 93.0 KB 93.2 KB ⚫ +0.2%
Server bundle size (gzip) vinext 157.0 KB 157.1 KB ⚫ +0.1%

View detailed results and traces

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

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

I re-reviewed after the two follow-up commits (5a50abe, 91dab97) that address the earlier blocking feedback. I verified the changes by running the touched test files (189 passed) and tsc --noEmit (clean).

Previous blocking issue is resolved

The out-of-basePath leak is fixed correctly. outOfBasePathRequestClaimed is now computed at app-rsc-handler.ts:657 (after beforeFiles, so a basePath: false beforeFiles rewrite still legitimately claims the request) and gates all three handlers that previously leaked:

  • image optimization (app-rsc-handler.ts:660)
  • metadata routes (app-rsc-handler.ts:675)
  • public files (app-rsc-handler.ts:689)

plus route matching (:728), POST/action dispatch (:738), and renderPagesForMatchKind (:805). The new parameterized test at tests/app-rsc-handler.test.ts:2096 asserts 404 for unclaimed out-of-basePath /logo.svg, /_next/image, and /favicon.ico, mirroring the in-basePath tests. Good coverage.

Correctness checks that pass

  • shouldEvaluateRule (config-matchers.ts:521-523) is the right primitive: with hadBasePath: false, default rules are skipped and only basePath: false rules evaluate. Threading the real hadBasePath through basePathState (replacing the hardcoded true) is consistent with prod-server.ts / pages-request-pipeline.ts.
  • redirectDestinationWithBasePath now correctly skips the prefix when !hadBasePath (:361).
  • Trailing-slash redirect is correctly suppressed for out-of-basePath requests (:540), so unclaimed requests don't get a basePath-prefixed redirect.
  • app-middleware.ts:240 hadBasePath: options.hadBasePath ?? true preserves backward compatibility; the new test confirms middleware sees nextUrl.basePath === "" / pathname === "/outside".
  • resolveActionRedirectTarget hard-navigates same-origin redirects that leave the basePath (app-browser-server-action-client.ts:96-99) — matches the Next.js v16.2.6 "redirect externally ... outside the basePath" test.
  • normalizeRscRequest's allowOutsideBasePath defaults to false, so the only caller that opts in is the App handler; no other path changes behavior.

Non-blocking: second stale comment missed

The earlier review fixed the stale comment at the old line 518, but there's a second one that now contradicts the code directly below it. app-rsc-handler.ts:350-353:

// TODO(#1333): once App Router supports basePath: false rules (see
// normalizeRscRequest — it 404s out-of-basePath requests before they
// reach this code), pass hadBasePath here and skip the prefix when
// false, mirroring the same guard in prod-server.ts and deploy.ts.

This PR ships exactly that support, and redirectDestinationWithBasePath already takes basePathState and skips the prefix when hadBasePath is false (:361). Please update or remove this TODO so it doesn't mislead future readers.

Non-blocking: narrow afterFiles/fallback parity gap

outOfBasePathRequestClaimed is computed once before the afterFiles/fallback rewrite loops (:836, :878), and those loops are not gated by it. For an external basePath: false rewrite (the case the Next.js test covers, e.g. destination: 'https://example.vercel.sh/') applyRewrite returns a Response and short-circuits at :853, so it works. App-route internal destinations also work because they fall through to the main render at :958 (which isn't gated).

The remaining gap: an unclaimed out-of-basePath request whose internal basePath: false afterFiles/fallback rewrite targets a Pages-router route would 404, because renderPagesForMatchKind early-returns null while !outOfBasePathRequestClaimed (:805) even after the rewrite legitimately claims the request. This is an obscure combination not exercised by the ported Next.js v16.2.6 test, so it's fine to leave for follow-up — but worth a tracking note rather than a silent divergence.

Verdict

The blocking issue from the prior round is properly fixed and tested. Only the stale TODO at :350-353 is worth cleaning up in this PR; the afterFiles/Pages gap can be a follow-up.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

The two follow-up commits (5a50abe, 91dab97) comprehensively resolved the earlier blocking review. I verified empirically — 189 tests pass across the four touched test files, and tsc --noEmit is clean.

Blocking issue resolved: The out-of-basePath leak is fixed. outOfBasePathRequestClaimed is hoisted to app-rsc-handler.ts:657 (after beforeFiles, so legitimate basePath: false beforeFiles rewrites still claim the request) and now gates image optimization, metadata routes, public files, route matching, POST/action dispatch, and the Pages fallback. New parameterized tests assert 404 for unclaimed out-of-basePath /logo.svg, /_next/image, and /favicon.ico.

Correctness confirmed: shouldEvaluateRule is the right primitive (default rules skipped, basePath: false rules evaluated when !hadBasePath); threading real hadBasePath matches prod-server.ts/pages-request-pipeline.ts; trailing-slash redirect correctly suppressed; server-action hard-navigation matches the Next.js v16.2.6 test; allowOutsideBasePath defaults to false so no other caller changes behavior.

Two non-blocking findings:

  1. A second stale comment at app-rsc-handler.ts:350-353 (the prior review only caught the one at the old line 518) — a TODO claiming App Router "does not yet support basePath: false rules" that directly contradicts the implementation right below it.
  2. A narrow parity gap: an internal basePath: false afterFiles/fallback rewrite targeting a Pages-router route from an unclaimed out-of-basePath request would 404, since the Pages fallback is gated by the once-computed outOfBasePathRequestClaimed. External and App-route destinations (the cases the ported Next.js test covers) work correctly, so this is fine as a documented follow-up.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

I re-reviewed after the two new follow-up commits (03e72bd track basePath rewrite claims, e84df94 remove stale basePath note). I verified by running the touched test files plus the middleware suites (228 tests passed across app-rsc-handler, app-browser-server-action-client, app-rsc-request-normalization, entry-templates, app-post-middleware-context, app-router-middleware-next-request, middleware-runtime) and vp check on the four touched server files (clean).

Both prior non-blocking findings are resolved

  1. Stale comments removed. Both stale #1333 comments are gone — the one at the old line 518 and the second one at the old :350-353. grep for does not yet support / #1333 / 404s out-of-basePath in app-rsc-handler.ts now returns nothing. redirectDestinationWithBasePath takes basePathState and correctly skips the prefix when !hadBasePath (:351-363).

  2. afterFiles/fallback → Pages parity gap closed. 03e72bd makes outOfBasePathRequestClaimed = true immediately after a successful afterFiles (:851) and fallback (:894) rewrite, before the subsequent renderPagesForMatchKind calls. Since the gate at :801 is re-checked per invocation (not once), an unclaimed out-of-basePath request whose internal basePath: false afterFiles/fallback rewrite targets a Pages route is now served instead of 404'd. The new parameterized test allows out-of-basePath %s rewrites to reach Pages routes (tests/app-rsc-handler.test.ts:184) exercises exactly this for both phases and asserts the /pages handoff.

Correctness checks that pass

  • The blocking leak fix from the first round holds: image optimization (:660), metadata routes (:668), public files (:685), route matching (:724), POST/action dispatch (:734), and renderPagesForMatchKind (:801) are all gated by outOfBasePathRequestClaimed. The 404 regression tests at :2143 cover /logo.svg, /_next/image, and /favicon.ico.
  • resolveActionRedirectTarget hard-navigates a same-origin redirect that leaves the basePath (app-browser-server-action-client.ts:96-99); hasBasePath matches on segment boundaries so /docsfoo won't falsely match /docs. deps.basePath was already part of the type.
  • normalizeRscRequest's allowOutsideBasePath defaults to false, so only the App handler opts in; hadBasePath is threaded into basePathState, the trailing-slash redirect (:536), and App middleware (hadBasePath: options.hadBasePath ?? true).

Non-blocking: one behavioral widening to flag

didMiddlewareRewrite changed from cleanPathname !== normalized.cleanPathname to middlewareResult.didRewrite (:615). The new flag is true for any middleware rewrite, including an identity rewrite (NextResponse.rewrite(req.nextUrl)) or a search-only rewrite, whereas the old pathname comparison treated those as false. This is load-bearing in two places: outOfBasePathRequestClaimed (correct — an explicit rewrite claims the request) and allowRscDocumentFallback: didMiddlewareRewrite (:806), which now permits the RSC→Pages-document fallback for identity/search-only middleware rewrites. This is arguably more correct (an explicit rewrite signals intent), no existing test regresses, and it isn't basePath-specific — but it's an incidental change to non-basePath behavior that isn't directly covered by a test. Worth a one-line note in the PR description or a focused test for the identity-rewrite case.

Verdict

Both items from the previous review are properly addressed and tested. No blocking issues. The only new observation is the non-blocking didMiddlewareRewrite widening above. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 after the two new commits since the last review round (03e72bd, e84df94). Both previously-flagged non-blocking items are now resolved, and I found no blocking issues.

Both prior findings resolved:

  1. Stale comments removed — both #1333 comments are gone; redirectDestinationWithBasePath now properly takes basePathState and skips the prefix when !hadBasePath.
  2. afterFiles/fallback → Pages parity gap closed03e72bd sets outOfBasePathRequestClaimed = true right after each successful afterFiles/fallback rewrite (before renderPagesForMatchKind), and a new parameterized test covers both phases.

Verified empirically: 228 tests pass across the touched test files plus middleware suites; vp check is clean on all four touched server files.

One new non-blocking observation: didMiddlewareRewrite changed from a pathname comparison to middlewareResult.didRewrite, which now treats identity/search-only middleware rewrites as "rewritten." This is load-bearing for allowRscDocumentFallback and is an incidental widening of non-basePath behavior — arguably more correct, no test regresses, but worth a note or focused test.

Verdict: LGTM, no blocking issues.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

Re-reviewed after the new commit 1cd6c8b ("allow late rewrite route posts"), the only change since the last review round. Verified empirically: all touched test files pass (97 + 97 across app-rsc-handler, app-browser-server-action-client, app-rsc-request-normalization, entry-templates).

The new commit is correct

1cd6c8b makes two gating changes so an out-of-basePath POST claimed by a late (afterFiles/fallback) basePath: false rewrite can reach an App route handler:

  1. app-rsc-handler.ts:734 — the early out-of-basePath POST 404 is narrowed from !claimed && isPost to !claimed && isPost && actionId. Previously every unclaimed out-of-basePath POST 404'd at the top, before the afterFiles/fallback loops ran, so a basePath: false rewrite to a route handler could never be reached on POST. The added && actionId lets a non-action POST fall through to the rewrite loops.

  2. app-rsc-handler.ts:739-744 — the progressive (multipart, no-actionId) action path now also requires outOfBasePathRequestClaimed, so an unclaimed out-of-basePath multipart POST is no longer treated as a progressive action.

I traced the unclaimed-no-actionId POST fall-through and confirmed it stays safe: every downstream handler (image opt :656, metadata :671, public files :685, preActionMatch :724, renderPagesForMatchKind :806) is still gated by outOfBasePathRequestClaimed, which remains false for a truly unclaimed request. The only new way such a POST produces a non-404 is a matching afterFiles/fallback basePath: false rewrite (:838/:881) — exactly the intended fix. A genuinely unclaimed POST still 404s, just via the !match not-found branch (:933) instead of the early notFoundResponse(), and clearRequestContext is still called (:955). The existing does not dispatch server actions directly outside basePath test (:258) still passes because it sends a next-action header.

The new parameterized test (tests/app-rsc-handler.test.ts:210) covers both the afterFiles and fallback phases for the route-handler handoff. Good coverage.

Intentional asymmetry worth noting (non-blocking)

An out-of-basePath server-action POST (with actionId) destined for a page via a late basePath: false rewrite still 404s at :734, because actionId is present and the request isn't yet claimed when that check runs. This is consistent with the existing design comment (:718-723: actions are processed against the cleanPathname they were posted to and do not re-run rewrites), and the ported Next.js v16.2.6 app-basepath suite passes 13/13. So the divergence (route handlers reachable via late rewrite, page actions not) appears intentional — but it's a subtle distinction that isn't directly asserted by a test. A one-line comment near :734 explaining why actionId gates the early 404 (i.e. actions don't re-run rewrites, route handlers do) would help future readers, and a negative test asserting the action-POST-to-page case still 404s would lock the boundary in.

Previously verified, still holds

  • Out-of-basePath leak fix: image opt / metadata / public files / route match / POST dispatch / renderPagesForMatchKind all gated by outOfBasePathRequestClaimed; 404 regression tests at :2212 cover /logo.svg, /_next/image, /favicon.ico.
  • /__vinext/ prerender endpoints are handled at :519-534, before any basePath gate, so the hadBasePath=false they now carry doesn't affect them.
  • normalizeRscRequest's allowOutsideBasePath defaults to false; only the App handler opts in.
  • resolveActionRedirectTarget hard-navigates same-origin redirects leaving the basePath (app-browser-server-action-client.ts:96-99).
  • Stale #1333 comments removed; redirectDestinationWithBasePath skips the prefix when !hadBasePath.

Verdict

The new commit is correct and tested. No blocking issues. Only non-blocking suggestion: a comment + negative test documenting the intentional action-POST-vs-route-handler asymmetry at :734.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 focused on the new commit 1cd6c8b ("allow late rewrite route posts"), the only change since the last review round. Earlier commits were already covered by prior reviews and remain correct.

The new commit is correct. It narrows the early out-of-basePath POST 404 (app-rsc-handler.ts:734) from !claimed && isPost to !claimed && isPost && actionId, and gates the progressive-action path on outOfBasePathRequestClaimed. This lets a non-action POST fall through to the afterFiles/fallback rewrite loops so a basePath: false rewrite can reach an App route handler on POST.

Verified empirically: all touched test files pass (194 tests across the four files). I traced the unclaimed-no-actionId POST fall-through and confirmed it stays safe — every downstream handler is still gated by outOfBasePathRequestClaimed, so the only new non-404 path is a legitimate matching late rewrite; a truly unclaimed POST still 404s (via the !match branch) with context cleared.

One non-blocking suggestion: there's an intentional asymmetry — an out-of-basePath server-action POST (with actionId) destined for a page via a late rewrite still 404s, while route handlers are reachable. This matches the existing design (actions don't re-run rewrites) and the ported Next.js v16.2.6 suite passes 13/13, but it's subtle and isn't directly asserted. I suggested a one-line comment near :734 plus a negative test to lock the boundary.

Verdict: no blocking issues.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

Re-reviewed after the new commit 8c4f4b0 ("short-circuit unclaimed basePath misses"), the only change since the last review round. Verified empirically: all touched test files pass (160 tests across app-rsc-handler, app-rsc-request-normalization, app-browser-server-action-client) and vp check on app-rsc-handler.ts is clean.

The core of the new commit is correct

8c4f4b0 adds an early-out at app-rsc-handler.ts:933-936: when a request reaches the bottom of the pipeline still !outOfBasePathRequestClaimed, return notFoundResponse() before invoking options.renderNotFound. This is a real optimization — previously an unclaimed out-of-basePath request fell to the if (!match) branch at :938 and compiled/rendered the app's not-found page even though Next.js never serves out-of-basePath requests from the app at all. The new test asserts renderNotFound is not called (tests/app-rsc-handler.test.ts:257).

I confirmed the placement is safe: match = preActionMatch and preActionMatch is null whenever !outOfBasePathRequestClaimed (:724). match only becomes truthy via the afterFiles/fallback rewrite loops, which always set outOfBasePathRequestClaimed = true first (:856, :899). So match truthy ⟹ claimed, and the short-circuit can never strand a legitimately-claimed late-rewrite match. The pagesDataRequest branch returns earlier (:917-931), so it's unaffected. In-basePath requests start with outOfBasePathRequestClaimed = hadBasePath = true (:595), so the short-circuit never fires for them.

Blocking-ish (parity regression): the short-circuit drops middleware response headers on the 404

The pre-existing not-found path at :960-963 merges middleware-contributed headers into the 404:

const headers = new Headers();
mergeMiddlewareResponseHeaders(headers, middlewareContext.headers);
return notFoundResponse({ headers });

The new short-circuit at :933-936 returns a bare notFoundResponse() with no header merge. This silently diverges from both Next.js and vinext's own in-basePath behavior, because middleware runs for out-of-basePath requests too (this PR's own test at tests/app-rsc-handler.test.ts:276 confirms middleware sees /outside). A NextResponse.next({ headers }) sets middlewareContext.headers without setting didRewrite (app-middleware.ts:264-266 vs :288), so such a request stays unclaimed and now loses its headers.

I reproduced this against the real handler (basePath /docs, middleware does NextResponse.next() and sets x-mw-custom: from-mw):

  • GET /outside (out-of-basePath, unclaimed) → 404 with x-mw-custom = null (dropped)
  • GET /docs/missing (in-basePath, unclaimed) → 404 with x-mw-custom = from-mw (preserved)

So identical middleware now yields different 404 headers depending solely on whether the path is inside the basePath. The docstring on notFoundResponse even calls out this exact contract — "The headers option lets call sites merge middleware response headers into the 404, matching the pattern used by app-rsc-handler after a route match fails but middleware has already contributed headers" (http-error-responses.ts:55-57) — and the new path bypasses it.

Suggested fix: merge middleware headers in the short-circuit, mirroring :960-963:

if (!outOfBasePathRequestClaimed) {
  options.clearRequestContext();
  const headers = new Headers();
  mergeMiddlewareResponseHeaders(headers, middlewareContext.headers);
  return notFoundResponse({ headers });
}

Please also add a regression test asserting an out-of-basePath unclaimed 404 carries a header set by middleware via NextResponse.next({ headers }), mirroring the in-basePath expectation. The ported Next.js v16.2.6 suite passes 13/13 but doesn't cover middleware-header-on-404, so this gap is currently silent.

Previously verified, still holds

  • Out-of-basePath leak fix: image opt / metadata / public files / route match / POST dispatch / renderPagesForMatchKind all gated by outOfBasePathRequestClaimed; the new short-circuit is consistent with that gating.
  • Late afterFiles/fallback rewrites correctly set the claim flag before renderPagesForMatchKind, so route-handler and Pages handoffs still work.
  • resolveActionRedirectTarget hard-navigates same-origin redirects leaving the basePath (app-browser-server-action-client.ts:96-99).
  • Stale #1333 comments removed; redirectDestinationWithBasePath skips the prefix when !hadBasePath.

Verdict

The short-circuit itself is correct and is a genuine improvement. The one issue is the dropped middleware response headers on the out-of-basePath 404 — a small, verified parity regression introduced by this commit. Worth fixing here (one-line merge + a focused test) so out-of-basePath and in-basePath 404s stay consistent.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 focused on the new commit 8c4f4b0 ("short-circuit unclaimed basePath misses"), the only change since the last review round. Earlier commits were already covered by prior reviews and remain correct.

The core change is correct. The early-out at app-rsc-handler.ts:933-936 returns 404 before invoking renderNotFound for unclaimed out-of-basePath requests. I verified the placement is safe — match can only be truthy when the request is already claimed (via the rewrite loops), so the short-circuit never strands a legitimate late-rewrite match, and in-basePath / pagesDataRequest paths are unaffected.

One verified parity regression (worth fixing here): The new short-circuit returns a bare notFoundResponse(), dropping middleware-contributed response headers, while the pre-existing not-found path at :960-963 merges them via mergeMiddlewareResponseHeaders. I reproduced this empirically against the real handler:

  • GET /outside (out-of-basePath, unclaimed) → 404, middleware header dropped
  • GET /docs/missing (in-basePath, unclaimed) → 404, middleware header preserved

So identical middleware yields inconsistent 404 headers based solely on basePath membership. This diverges from Next.js (middleware runs for out-of-basePath requests and its NextResponse.next({ headers }) headers apply to 404s) and contradicts the documented contract on notFoundResponse itself. Suggested a one-line header merge plus a focused regression test. The ported Next.js v16.2.6 suite passes 13/13 but doesn't cover middleware-header-on-404, so this gap is currently silent.

Verdict: The short-circuit is a genuine improvement; the only issue is the dropped middleware headers on the out-of-basePath 404.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review: fix(app-router): honor basePath false rewrites (#2370)

Re-reviewed after the new commit cda8dc7 ("preserve out-of-basePath 404 semantics"), the only change since the last round. The prior blocking issue (dropped middleware headers on the out-of-basePath 404) is now fixed, and the cache-busting redirect is now gated by basePath. Verified empirically: touched test files pass (162 across app-rsc-handler, app-rsc-request-normalization, app-browser-server-action-client), middleware suites pass (70), and vp check is clean on the four touched server files.

Prior blocking issue is resolved

cda8dc7 adds the middleware-header merge to the short-circuit 404 at app-rsc-handler.ts:940-944, mirroring the post-match not-found path at :970-972. The new test preserves middleware response headers on unclaimed out-of-basePath 404s (tests/app-rsc-handler.test.ts:273) confirms an out-of-basePath unclaimed 404 now carries NextResponse.next({ headers }) headers, so out-of-basePath and in-basePath 404s are consistent again.

Non-blocking (verified): RSC cache-busting is skipped for late-claimed out-of-basePath requests

This commit also moves RSC cache-busting validation behind basePath. The unconditional check now only runs when hadBasePath (:578-580), and a second "claimed" check (:655-661) runs for out-of-basePath requests — but only after the beforeFiles loop and before the afterFiles/fallback loops. outOfBasePathRequestClaimed can also flip to true later, in afterFiles (:863) and fallback (:906). Those late-claimed requests never hit the cache-busting check, so an out-of-basePath RSC GET that lands on a real App route via a late basePath: false rewrite renders without _rsc validation, while the identical request claimed by beforeFiles gets the cache-busting redirect.

I reproduced this against the real handler (basePath /docs, RSC GET to /outside with a deliberately wrong _rsc cache-busting param, basePath: false rewrite /outside → /inside which is a real App route):

  • beforeFiles rewrite → 307 redirect to /outside?_rsc=... (cache-busting fires) ✅
  • afterFiles rewrite → 200, page rendered directly, no _rsc validation ❌
  • in-basePath control (/docs/inside) → 307 (cache-busting fires) ✅

So the same logical request gets cache-busting validation depending solely on which rewrite phase claims it. Pre-PR this couldn't happen (out-of-basePath requests 404'd in normalizeRscRequest and never reached cache-busting or rendering), so this is a newly-introduced inconsistency rather than a pre-existing gap.

It's narrow (out-of-basePath + RSC GET + afterFiles/fallback basePath: false rewrite onto an App route) and not covered by the ported Next.js v16.2.6 suite, so it's reasonable as a documented follow-up rather than a release blocker. The clean fix would be to also run the claimed cache-busting check immediately after a late rewrite sets outOfBasePathRequestClaimed = true (before renderPagesForMatchKind / the final render), mirroring how 03e72bd set the claim flag in those same loops. A regression test asserting the afterFiles/fallback-claimed RSC GET redirects (matching the beforeFiles behavior) would lock the boundary.

Previously verified, still holds

  • Out-of-basePath leak fix: image opt (:663), metadata (:678), public files (:692), route match (:731), POST/action dispatch (:741), and renderPagesForMatchKind (:813) are all gated by outOfBasePathRequestClaimed; the new short-circuit at :940 is consistent with that gating and now merges middleware headers.
  • Late afterFiles/fallback rewrites set the claim flag before renderPagesForMatchKind, so route-handler and Pages handoffs work (tests at :179, :210).
  • resolveActionRedirectTarget hard-navigates same-origin redirects leaving the basePath; hasBasePath matches on segment boundaries (base-path.ts:12-15) so /docsfoo won't falsely match /docs.
  • hadBasePath is threaded consistently into basePathState, the trailing-slash redirect (:536), redirectDestinationWithBasePath, and App middleware (hadBasePath: options.hadBasePath ?? true).
  • normalizeRscRequest's allowOutsideBasePath defaults to false; only the App handler opts in. Both stale #1333 comments remain removed.
  • /__vinext/ prerender endpoints are handled at :519-534, before any basePath gate.

Verdict

The prior blocking issue is fixed correctly and tested. No blocking issues. One non-blocking, empirically-verified inconsistency: RSC cache-busting is applied to beforeFiles-claimed but not afterFiles/fallback-claimed out-of-basePath requests — worth a follow-up fix + test, but acceptable to defer given how narrow it is.

github run

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 after the new commit cda8dc7 ("preserve out-of-basePath 404 semantics"), the only change since the last round. Verified empirically: 162 tests pass across the touched files, 70 across the middleware suites, and vp check is clean on all four touched server files.

Prior blocking issue resolved: The dropped-middleware-headers regression on the out-of-basePath 404 is fixed — cda8dc7 merges middleware headers into the short-circuit 404 (app-rsc-handler.ts:940-944), and the new test at tests/app-rsc-handler.test.ts:273 confirms it.

One new non-blocking finding (verified empirically): This commit gates RSC cache-busting behind basePath, but the "claimed" cache-busting check (:655-661) runs only after the beforeFiles loop — not after afterFiles/fallback. So an out-of-basePath RSC GET claimed by a late basePath: false rewrite onto a real App route renders without _rsc validation, while a beforeFiles-claimed one gets the cache-busting redirect. I reproduced all three cases against the real handler (beforeFiles → 307, afterFiles → 200 no validation, in-basePath control → 307). It's a newly-introduced inconsistency but extremely narrow and not covered by the ported Next.js suite — reasonable as a documented follow-up. Suggested fix: run the claimed cache-busting check right after a late rewrite sets the claim flag, mirroring 03e72bd.

Verdict: No blocking issues. The prior blocking issue is properly fixed and tested.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

Re-reviewed after the new commit 7040f3f ("validate late basePath rewrites"), the only change since the last review round. Verified empirically: all touched test files pass (200 tests across app-rsc-handler, app-rsc-request-normalization, app-browser-server-action-client, entry-templates) and vp check on app-rsc-handler.ts is clean.

The prior blocking + non-blocking items are resolved

The previously-flagged RSC cache-busting inconsistency is fixed. The "claimed" cache-busting check now also runs inside the afterFiles (app-rsc-handler.ts:918-924) and fallback (:968-974) loops, immediately after each late rewrite sets outOfBasePathRequestClaimed = true — mirroring the beforeFiles check at :655-661. The new parameterized test validates out-of-basePath RSC requests claimed by %s rewrites (tests/app-rsc-handler.test.ts:286) asserts the 307 + recomputed _rsc for both phases. I confirmed resolveInvalidRscCacheBustingRequest early-returns for non-GET/HEAD (app-rsc-cache-busting.ts:350-351), so the omission of this check from the new action loop is correct.

New behavior: out-of-basePath Server Actions now re-run late rewrites

7040f3f adds a dedicated pre-dispatch rewrite loop (:667-718) so an unclaimed out-of-basePath action POST (isPost && actionId) can be claimed by an afterFiles/fallback basePath: false rewrite and dispatched against the rewritten cleanPathname. The subsequent main rewrite loops are then skipped via the new resolvedLateRewritesForAction guard (:898, :948) so rewrites aren't applied twice. I traced this and it's correct:

  • The early POST 404 at :795 no longer fires once the loop claims the request.
  • A genuinely-unclaimed action POST (no matching late rewrite) leaves outOfBasePathRequestClaimed false and still 404s at :795.
  • An action POST whose late rewrite source matches but whose destination has no route (actionMatch null) is claimed, dispatch returns null, match stays null, and it falls through to renderNotFound at :1015 — reasonable.

The new test (:265) covers both phases and asserts handleServerActionRequest is called with the rewritten cleanPathname: "/about".

Blocking-ish: stale design comment now contradicts the code

app-rsc-handler.ts:786-787 still says:

For action requests we intentionally do not re-run rewrites — actions are always processed against the cleanPathname they were posted to.

That was accurate as of 1cd6c8b (and was explicitly called out in the prior review as the intentional action-POST-vs-route-handler asymmetry), but 7040f3f reverses it: out-of-basePath action POSTs do now re-run afterFiles/fallback rewrites before dispatch (:667-718), and the new test asserts an action posted to /outside is dispatched against the rewritten /about. The comment now directly contradicts the loop a few lines above it. Please update it to describe the actual behavior (out-of-basePath actions re-run late rewrites to claim the request; in-basePath actions are processed against the posted cleanPathname), so future readers aren't misled. This is the same class of stale-comment issue the earlier rounds removed for the #1333 notes.

Non-blocking: action loop duplicates the main rewrite loops

The new :667-718 loop is a near-copy of the afterFiles/fallback loops at :898-990 (same applyRewrite args, mergeRewriteQuery, pathnameForResolvedUrl, matchRoute cadence), differing only in that it omits renderPagesForMatchKind and the cache-busting check (both correctly N/A for action POSTs). Two copies of the rewrite-claim sequence are easy to drift out of sync on a future fix. Per AGENTS.md's "generated entries should stay thin / move real behavior into typed helpers" guidance, consider extracting a small claimViaLateRewrites({ rewrites, ... }) helper that both call. Not required for this PR.

Non-blocking (pre-existing, not introduced here): action POST → metadata/public-file via late rewrite

Because the action loop runs before the image/metadata/public-file handlers (:720-760) and handleMetadataRouteRequest doesn't gate on method, an out-of-basePath action POST rewritten onto e.g. /sitemap.xml would serve the metadata route on a POST. This mirrors the existing in-basePath ordering (the metadata handler already runs before action dispatch for claimed requests), so it's not a regression from this PR — just worth a tracking note if method-gating metadata routes is ever desired.

Previously verified, still holds

  • Out-of-basePath leak fix: image opt (:720), metadata (:735), public files (:749), route match (:788), POST/action dispatch (:795), and renderPagesForMatchKind (:867) are all gated by outOfBasePathRequestClaimed; the short-circuit 404 at :1008-1012 merges middleware headers.
  • Late afterFiles/fallback rewrites set the claim flag before renderPagesForMatchKind, so route-handler/Pages handoffs work.
  • resolveActionRedirectTarget hard-navigates same-origin redirects leaving the basePath; hasBasePath matches on segment boundaries.
  • /__vinext/ prerender endpoints handled at :519-534 before any basePath gate, so the action POST loop doesn't intercept them.
  • normalizeRscRequest's allowOutsideBasePath defaults to false; both stale #1333 comments remain removed.

Verdict

The new commit correctly resolves the prior non-blocking RSC cache-busting finding (with tests for both phases) and adds correct, tested late-rewrite support for out-of-basePath action POSTs. The one thing worth fixing in this PR is the stale comment at :786-787, which now contradicts the new action-rewrite behavior. The loop duplication and the metadata-on-POST note are non-blocking.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 focused on the new commit 7040f3f ("validate late basePath rewrites"), the only change since the last review round. Verified empirically: 200 tests pass across the four touched test files, and vp check on app-rsc-handler.ts is clean.

Prior non-blocking finding resolved: The RSC cache-busting inconsistency is fixed — the claimed cache-busting check now runs inside the afterFiles (:918-924) and fallback (:968-974) loops too, matching the beforeFiles check. New parameterized tests assert 307 + recomputed _rsc for both phases.

New behavior verified correct: A dedicated pre-dispatch loop (:667-718) lets unclaimed out-of-basePath action POSTs be claimed by late basePath: false rewrites, guarded by resolvedLateRewritesForAction to avoid double-applying rewrites. Genuinely unclaimed POSTs still 404.

One thing worth fixing in this PR (blocking-ish): The design comment at :786-787 ("For action requests we intentionally do not re-run rewrites") now directly contradicts the new loop — 7040f3f makes out-of-basePath actions re-run late rewrites. This is the same stale-comment class the earlier rounds cleaned up. Should be updated.

Non-blocking: the new action loop is a near-duplicate of the main afterFiles/fallback loops (drift risk — consider a shared helper), and a pre-existing metadata-on-POST ordering note.

Verdict: Commit is correct and tested; only the stale comment at :786-787 is worth fixing here.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

Re-reviewed after the new commit 8c23bfe ("docs(app-router): clarify late action rewrites"), the only change since the last review round. Verified empirically: 166 tests pass across the three touched server test files (app-rsc-handler, app-rsc-request-normalization, app-browser-server-action-client) and vp check on app-rsc-handler.ts is clean.

Prior blocking item resolved

The stale design comment flagged last round (app-rsc-handler.ts:786-787) is fixed. It previously said:

For action requests we intentionally do not re-run rewrites — actions are always processed against the cleanPathname they were posted to.

…which contradicted the late-action-rewrite loop added in 7040f3f. The new comment (:782-786) now accurately describes the behavior:

The route is matched against the current cleanPathname here. Ordinary requests may still be rewritten by the afterFiles / fallback loops below, where the second setRootParams call replaces this value before rendering. Out-of-basePath Server Actions resolve those late rewrites above so this match already uses their claimed destination.

I grepped for the prior stale-comment patterns (does not yet support, #1333, intentionally do not re-run, 404s out-of-basePath) — none remain. The comment is correct: out-of-basePath action POSTs are claimed by the pre-dispatch loop at :667-718 (which sets resolvedLateRewritesForAction), so preActionMatch at :787 already sees the rewritten destination.

Full re-verification (all still holds)

  • Out-of-basePath leak fix: image opt (:720), metadata routes (:735), public files (:749), route match (:787), POST/action dispatch (:794), and renderPagesForMatchKind (:866) are all gated by outOfBasePathRequestClaimed.
  • Late rewrite claims: beforeFiles (:649), the action pre-dispatch loop (:686/:710), afterFiles (:916), and fallback (:966) all flip the claim flag and (for non-basePath) run the claimed RSC cache-busting check (:655-661, :917-923, :967-973).
  • Action loop guard: resolvedLateRewritesForAction (:717) correctly suppresses the later afterFiles (:897) and fallback (:947) loops so rewrites aren't applied twice.
  • 404 semantics: both not-found paths (:1007-1012 short-circuit, :1037-1039 post-render) merge middleware headers via mergeMiddlewareResponseHeaders, keeping out-of-basePath and in-basePath 404s consistent.
  • Trailing-slash redirect suppressed for out-of-basePath (:536); redirect path uses redirectDestinationWithBasePath with basePathState and skips the prefix when !hadBasePath.
  • /__vinext/ prerender endpoints handled at :519-534 before any basePath gate.
  • normalizeRscRequest allowOutsideBasePath is opt-in (defaults false; only the App handler opts in via the true arg at :491).
  • resolveActionRedirectTarget hard-navigates same-origin redirects leaving the basePath; matches the ported Next.js v16.2.6 test.

Non-blocking (carried over, not introduced by this commit)

  • Action-loop duplication: the :667-718 pre-dispatch loop is still a near-copy of the afterFiles/fallback loops at :897-989. Per AGENTS.md's "move real behavior into typed helpers" guidance, extracting a shared claimViaLateRewrites(...) helper would reduce drift risk. Not required for this PR.
  • Metadata-on-POST ordering: an out-of-basePath action POST rewritten onto a metadata route (e.g. /sitemap.xml) would serve it on POST, mirroring the existing in-basePath ordering — not a regression here.

Verdict

The prior blocking stale-comment issue is fully resolved and the comment now matches the implementation. No blocking issues. The two non-blocking items are pre-existing and reasonable to defer. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 after the new commit 8c23bfe ("docs(app-router): clarify late action rewrites"), the only change since the last review round. Verified empirically: 166 tests pass across the three touched server test files and vp check on app-rsc-handler.ts is clean.

Prior blocking item resolved: The stale comment at :786-787 that contradicted the late-action-rewrite loop (added in 7040f3f) is fixed. The new comment now accurately describes that out-of-basePath Server Actions resolve late rewrites before the preActionMatch. I grepped for all prior stale-comment patterns — none remain.

Full re-verification: The out-of-basePath leak fix, late-rewrite claim flags, the resolvedLateRewritesForAction double-rewrite guard, 404 middleware-header merging on both not-found paths, trailing-slash suppression, prerender-endpoint handling, and resolveActionRedirectTarget hard-navigation all still hold.

Two non-blocking items (pre-existing, not introduced by this commit): the action-loop duplication (candidate for a shared helper) and metadata-on-POST ordering.

Verdict: LGTM — no blocking issues.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/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: fix(app-router): honor basePath false rewrites (#2370)

Re-reviewed after the new commit 6d9ecd5 ("resolve progressive action rewrites"), the only change since the last review round. Verified empirically: 202 tests pass across the four touched test files (app-rsc-handler 105, app-browser-server-action-client 2, app-rsc-request-normalization 61, entry-templates 34), and vp check on app-rsc-handler.ts is clean.

The new commit is correct

6d9ecd5 extends the early out-of-basePath late-rewrite loop so a progressive Server Action (multipart POST with no actionId) can be claimed by an afterFiles/fallback basePath: false rewrite, mirroring what 7040f3f already did for actionId-based actions. Two changes:

  1. app-rsc-handler.ts:666-668contentType is hoisted above the loop and a new isProgressiveActionRequest = isPostRequest && !actionId && contentType.startsWith("multipart/form-data") is computed. This signature exactly matches the existing progressive-action detection used by the dispatch block (:803) and app-action-request.ts:15, so it's parity-consistent — a multipart POST without an action header is the canonical progressive-action shape in Next.js.
  2. app-rsc-handler.ts:670 — the early claim loop guard widens from isPostRequest && actionId to (actionId || isProgressiveActionRequest), so a progressive POST now also resolves late rewrites before dispatch and sets resolvedLateRewritesForAction to suppress the duplicate afterFiles/fallback loops below (:898, :948).

I traced the boundaries and they hold:

  • In-basePath unaffected. outOfBasePathRequestClaimed starts as hadBasePath (:594), so for in-basePath requests it's true and the early loop (gated by !outOfBasePathRequestClaimed) never runs. The change is strictly out-of-basePath scoped.
  • Genuinely-unclaimed progressive POST still 404s. A multipart POST to an out-of-basePath path with no matching late rewrite leaves outOfBasePathRequestClaimed false: the early action 404 at :795 doesn't fire (it's actionId-gated and actionId is null), the progressive dispatch block at :800 requires outOfBasePathRequestClaimed (false) and is skipped, and the request falls through to the unclaimed short-circuit 404 at :1008-1012 with middleware headers merged. Correct.
  • contentType ordering is safe — it's now declared once at :666 and every read (:668, :803, :810, :849) is after the declaration; the old :792 declaration was removed.

The new parameterized test allows out-of-basePath progressive Server Actions through %s rewrites (tests/app-rsc-handler.test.ts:286) covers both phases and asserts handleProgressiveActionRequest is called with the rewritten cleanPathname: "/about". Good coverage of the positive case.

Non-blocking: no negative test for the unclaimed progressive POST

The existing does not dispatch server actions directly outside basePath test (:396) only covers the actionId path. The widened early-loop guard now also admits multipart-no-actionId POSTs, but there's no test asserting that an unclaimed out-of-basePath progressive POST (multipart, no matching basePath: false rewrite) still 404s without invoking handleProgressiveActionRequest. I confirmed this behavior holds by tracing the code, but a one-line negative test would lock the boundary, mirroring the existing actionId negative test. Not required for this PR.

Non-blocking (carried over, not introduced here)

  • Action-loop duplication: the :670-721 early loop remains a near-copy of the afterFiles/fallback loops at :898-990. Per AGENTS.md's "move real behavior into typed helpers" guidance, a shared claimViaLateRewrites(...) helper would reduce drift risk — and this commit slightly increased the surface that depends on the two staying in sync. Still a follow-up, not a blocker.
  • Metadata-on-POST ordering: an out-of-basePath progressive POST rewritten onto a metadata route would serve it on POST, mirroring the existing in-basePath ordering — not a regression here.

Previously verified, still holds

  • Out-of-basePath leak fix: image opt (:723), metadata routes (:738), public files (:752), route match (:790), POST/action dispatch (:795), and renderPagesForMatchKind (:867) are all gated by outOfBasePathRequestClaimed.
  • Both not-found paths (:1008-1012 short-circuit, :1037-1040 post-render) merge middleware headers, keeping out-of-basePath and in-basePath 404s consistent.
  • Claimed RSC cache-busting runs after beforeFiles (:655-661), afterFiles (:918-924), and fallback (:968-974); correctly omitted from the action loop since actions are POST (non-GET/HEAD).
  • resolvedLateRewritesForAction (:720) suppresses the later afterFiles/fallback loops so rewrites aren't applied twice.
  • Stale #1333 comments remain removed; redirectDestinationWithBasePath skips the prefix when !hadBasePath; /__vinext/ prerender endpoints handled before any basePath gate; resolveActionRedirectTarget hard-navigates same-origin redirects leaving the basePath.

Verdict

The new commit correctly and symmetrically extends late-rewrite claiming to progressive Server Actions, matching the prior actionId work, and is well-tested for the positive case. No blocking issues. The only suggestion is a negative test for the unclaimed progressive POST; the loop-duplication and metadata-on-POST items are pre-existing follow-ups. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2370.

Summary

I re-reviewed PR #2370 after the new commit 6d9ecd5 ("resolve progressive action rewrites"), the only change since the last review round. Verified empirically: 202 tests pass across the four touched test files and vp check on app-rsc-handler.ts is clean.

The new commit is correct. It symmetrically extends the early out-of-basePath late-rewrite loop so a progressive Server Action (multipart POST without actionId) can be claimed by an afterFiles/fallback basePath: false rewrite — mirroring what 7040f3f already did for actionId-based actions. The new isProgressiveActionRequest detection (:666-668) matches the canonical progressive-action signature used elsewhere in the codebase, and contentType was safely hoisted (no reference-before-declaration). I traced the boundaries:

  • In-basePath behavior is unaffected (early loop is !outOfBasePathRequestClaimed-gated).
  • A genuinely-unclaimed progressive POST still 404s via the short-circuit at :1008-1012 with merged middleware headers.
  • The positive case is well-tested for both rewrite phases (tests/app-rsc-handler.test.ts:286).

No blocking issues. Non-blocking suggestions:

  1. Add a negative test for the unclaimed progressive POST (the existing negative test at :396 only covers the actionId path).
  2. Pre-existing: the early action loop is a near-duplicate of the afterFiles/fallback loops — a shared helper would reduce drift risk.

Verdict: LGTM.

github run

@james-elicx james-elicx marked this pull request as ready for review June 26, 2026 08:18
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.

1 participant