Skip to content

fix: match bare basePath root for catch-all middleware matchers#94304

Open
Xavrir wants to merge 1 commit into
vercel:canaryfrom
Xavrir:fix-73786-middleware-matcher-basepath-root
Open

fix: match bare basePath root for catch-all middleware matchers#94304
Xavrir wants to merge 1 commit into
vercel:canaryfrom
Xavrir:fix-73786-middleware-matcher-basepath-root

Conversation

@Xavrir
Copy link
Copy Markdown

@Xavrir Xavrir commented Jun 1, 2026

What

When basePath is set, middleware did not run on the bare basePath root. With

// middleware config
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

and basePath: '/docs' in next.config, a request to /docs skipped
middleware, while /docs/ and /docs/foo ran it. Without a basePath the same
matcher runs on /. Fixes #73786.

Why

getMiddlewareMatchers in packages/next/src/build/analysis/get-page-static-info.ts
builds each matcher's source and then prepends basePath. A catch-all like
/((?!api).*) matches / because its one capture group can be empty, so the
compiled regex is ^(?:/(...))(...)?[/#?]?$ and / matches.

Once basePath is prepended the regex becomes ^/docs(?:/(...))..., which
requires a slash right after /docs. The bare /docs then fails to match even
though / matched before. The matcher is baked into the middleware manifest, so
this affects both the dev server and production (including Vercel), not just one
runtime.

How

Before prepending basePath, check whether the matcher regex (without basePath)
matches /. If it does, after building the final regex also accept the bare
basePath root by OR-ing in an anchored alternative:

^(?:/docs[\/#\?]?$|<original-body>)

The check is gated on basePath being set and the matcher not being the literal
/ root (which already has its own handling). Specific matchers like
/dashboard and param matchers like /:id do not match /, so they are left
unchanged and do not start matching the basePath root.

Tests

Extended get-page-static-info.test.ts with three cases:

  • catch-all matcher with basePath: '/docs': /docs, /docs/, /docs/foo
    match; /docs/api/x, /docsfoo, /foo do not.
  • static /dashboard: /docs/dashboard matches; /docs and /docs/ do not.
  • param /:id: /docs/apple matches; /docs does not.

All nine tests in the file pass (six existing plus the three new ones):

pnpm jest packages/next/src/build/analysis/get-page-static-info.test.ts
Tests: 9 passed, 9 total

prettier and eslint were run on both changed files.

Signed commits

The commit is signed (SSH) and shows as verified, so it meets the protected
branch requirement.

// without a `basePath`. Restore that by also accepting the bare
// `basePath` root. See https://github.com/vercel/next.js/issues/73786.
if (matchesRoot) {
const basePathRoot = `${escapeStringRegexp(nextConfig.basePath!)}[\\/#\\?]?$`
Copy link
Copy Markdown
Contributor

@vercel vercel Bot Jun 1, 2026

Choose a reason for hiding this comment

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

The bare-basePath-root middleware matcher restoration omits transport-route forms, so the root RSC/data request (e.g. /docs.rsc) does not match even though the no-basePath analog /.rsc does.

Fix on Vercel

When `basePath` is set, `getMiddlewareMatchers` prepends it to each matcher
source. For a catch-all matcher like `/((?!api).*)` that previously matched the
site root `/`, the prepended form becomes anchored at `${basePath}/...`, so the
bare `basePath` root (e.g. `/docs`) no longer matches even though `/` did
without a basePath. Middleware therefore did not run on the basePath root.

Detect when a matcher matches `/` before prepending basePath, and in that case
also accept the bare basePath root by OR-ing in an anchored
`^${basePath}[/#?]?$` alternative. Specific and param matchers are unaffected.

Adds regression tests covering catch-all, static, and param matchers.

Closes vercel#73786
@Xavrir Xavrir force-pushed the fix-73786-middleware-matcher-basepath-root branch from 0d986f7 to d3dec31 Compare June 1, 2026 14:06
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.

Middleware matcher does not catch root path if base path is set

1 participant