Skip to content

fix(basepath): enforce scoping on rewrites/redirects/routes#1397

Merged
james-elicx merged 4 commits into
mainfrom
fix/issue-1333-basepath-scoping
May 21, 2026
Merged

fix(basepath): enforce scoping on rewrites/redirects/routes#1397
james-elicx merged 4 commits into
mainfrom
fix/issue-1333-basepath-scoping

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

Sub-issues addressed under #1333:

  • Sub-issue 1 (rewrites/redirects/headers gating). Default rules now only fire when the request is under basePath, and basePath: false opt-out rules only fire when the request is outside it. Implemented by threading a BasePathMatchState through matchRewrite / matchRedirect / matchHeaders and every caller (Pages Router prod-server, App Router app-rsc-handler, the Cloudflare worker entry in deploy.ts, and the dev plugin).
  • Sub-issue 5 (404 outside basePath). Pages Router (both prod-server.ts and the generated worker in deploy.ts) now returns 404 for out-of-basepath requests after rewrites have had a chance to fire. App Router already had this via normalizeRscRequest. Matches Next.js's resolve-routes.ts:304-309.
  • Sub-issue 4 (basePath doubling). The doubling was a side-effect of the rewrite gating bug — out-of-basepath requests matched /rewrite-1 style rules, then the redirect-destination code prepended basePath again. Fixed once Virtual module imports break esbuild dependency optimization when vinext is installed from npm #1 lands. Pure-asset doubling (if any) is tracked separately.

Deferred (called out in the issue):

References:

Refs #1333 (not Closes — sub-issues 2 and 3 remain).

Test plan

  • Added 13 unit tests in tests/nextjs-compat/basepath.test.ts covering each gating case (default + basePath: false, in + out of basePath, all three matchers, plus backward-compat without an explicit state arg). All 23 tests in that file pass.
  • vp test run tests/shims.test.ts tests/next-config.test.ts tests/nextjs-compat/basepath.test.ts — 1065 pass.
  • vp test run tests/app-router.test.ts — 318 pass.
  • vp test run tests/pages-router.test.ts — 209 pass.
  • vp test run tests/features.test.ts — 303 pass.
  • vp check --fix — lint + types clean.
  • CI: full Vitest + Playwright E2E (Pages Router + App Router projects) to confirm the upstream test/e2e/basepath/* failures drop.

Sub-issues addressed under #1333:

1. Rewrites/redirects/headers were firing on requests outside the
   configured `basePath`. Threaded a `BasePathMatchState` through the
   matchers and prod-server / deploy worker / dev plugin so default
   rules (no `basePath: false` opt-out) only evaluate when the request
   was under basePath, and opt-out rules only evaluate outside it.

5. Pages Router requests that landed outside basePath fell through to
   internal route matching. After redirects and beforeFiles rewrites
   run, the request now 404s when it was outside basePath and no
   `basePath: false` rule rewrote it — matching Next.js's
   `resolve-routes.ts:304-309`.

4. With rewrites no longer firing on out-of-basepath requests, the
   downstream code that prepends basePath to destinations no longer
   sees a stripped-but-already-internal pathname, eliminating the
   `/docs/docs/...` doubling for the rewrite-driven cases.

App Router uses the same gating; `basePath: false` rules don't yet
fire there because `normalizeRscRequest` 404s out-of-basepath requests
before the matchers see them — flagged as follow-up in the rule
gating site. External rewrite proxying (sub-issue #2) and static
asset doubling (sub-issue #3, partly addressed by #1337/#1383) are
deferred.

Refs #1333
@pkg-pr-new

pkg-pr-new Bot commented May 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 49b1c46

@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
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review

@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.

Clean, well-scoped change. The shouldEvaluateRule gating logic correctly implements Next.js semantics from load-custom-routes.ts:530-580, the threading through all four request-handling paths (App Router, Pages Router prod, Cloudflare worker, dev server) is consistent, and the 404 gate for out-of-basePath requests matches resolve-routes.ts:304-309.

A few observations:

  1. Test completeness (minor): The test suite covers basePath: false rewrite under basePath → null (line 318), but the equivalent inverse case is missing for redirects and headers (basePath: false rule when request IS under basePath → should NOT match). The logic is shared via shouldEvaluateRule so this isn't a correctness risk, but adding these two cases would round out the test matrix.

  2. App Router redirect destinations are safe today but fragile for the follow-up. redirectDestinationWithBasePath in app-rsc-handler.ts:343 unconditionally prepends basePath. This works because hadBasePath is hardcoded to true — but when sub-issue 2 makes hadBasePath dynamic (to support basePath: false rules in App Router), the redirect destination path will need the same hadBasePath && ... guard that prod-server.ts:1600 and deploy.ts:685 have. Worth a // TODO(#1333): comment to flag it.

  3. The configRewriteFired 404 gate only checks beforeFiles, not afterFiles/fallback. This is correct per Next.js semantics (the 404 fires before filesystem routing), but since it's non-obvious, the inline comment in deploy.ts could benefit from explicitly stating that afterFiles and fallback intentionally run after this gate, not before it.

All nits, the change is solid. CI fully green.

basePath: "/docs",
hadBasePath: false,
});
expect(result).toEqual({ destination: "/another-destination", permanent: false });

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.

Nit: consider adding inverse tests for basePath: false redirect and header when the request IS under basePath (should NOT match). You have this case for rewrites at line 318 ("basePath: false rewrite rule does NOT match when request is under basePath") but the analogous redirect and header tests are missing. Since shouldEvaluateRule is shared this is low-risk, but it rounds out the matrix.

basePathState,
);
if (redirect) {
const destination = sanitizeDestination(

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.

When the sub-issue 2 follow-up makes hadBasePath dynamic (to support basePath: false rules in App Router), this redirectDestinationWithBasePath call will need the same hadBasePath && guard that prod-server.ts:1600 and deploy.ts:685 have. Consider adding a // TODO(#1333): here to flag it for the follow-up.

@ask-bonk

ask-bonk Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

The review has been posted. Here's a summary:

Review posted on PR #1397 — Approved with two inline nits.

Verdict: APPROVE. The change is well-structured and correct. The core shouldEvaluateRule gating logic correctly implements Next.js semantics, and it's threaded consistently through all four request-handling paths (App Router, Pages Router prod, Cloudflare worker, dev server). The 404 gate for out-of-basePath requests matches Next.js's resolve-routes.ts:304-309. CI is fully green.

Two minor inline comments posted:

  1. tests/nextjs-compat/basepath.test.ts:370 — Missing inverse tests for basePath: false redirect and header when the request IS under basePath (should NOT match). The rewrite test has this case but redirects and headers don't. Low risk since shouldEvaluateRule is shared.

  2. packages/vinext/src/server/app-rsc-handler.ts:343redirectDestinationWithBasePath unconditionally prepends basePath, which is safe today because hadBasePath is hardcoded true. But when sub-issue 2 makes this dynamic, it'll need the same hadBasePath && guard as prod-server and deploy. Suggested adding a // TODO(#1333): comment.

github run

@james-elicx james-elicx marked this pull request as ready for review May 20, 2026 21:05
@james-elicx james-elicx enabled auto-merge (squash) May 21, 2026 13:49
@james-elicx james-elicx merged commit b7f7ad6 into main May 21, 2026
36 checks passed
@james-elicx james-elicx deleted the fix/issue-1333-basepath-scoping branch May 21, 2026 13:49
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