Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/next/src/build/analysis/get-page-static-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,51 @@ describe('get-page-static-infos', () => {
regex.test('/index.segments/$c$children/__PAGE__.segment.rsc')
).toBe(true)
})

it('matches the bare basePath root for a catch-all matcher', () => {
// Regression test for https://github.com/vercel/next.js/issues/73786
const matcher = '/((?!api|_next/static|_next/image|favicon.ico).*)'
const regex = new RegExp(
getMiddlewareMatchers(matcher, {
i18n: undefined,
basePath: '/docs',
})[0].regexp
)

// The bare basePath root must match (previously it did not).
expect(regex.test('/docs')).toBe(true)
expect(regex.test('/docs/')).toBe(true)
expect(regex.test('/docs/foo')).toBe(true)
// Negative lookahead and word boundaries must still hold.
expect(regex.test('/docs/api/x')).toBe(false)
expect(regex.test('/docsfoo')).toBe(false)
expect(regex.test('/foo')).toBe(false)
})

it('does not match the bare basePath root for a specific matcher', () => {
const regex = new RegExp(
getMiddlewareMatchers('/dashboard', {
i18n: undefined,
basePath: '/docs',
})[0].regexp
)

expect(regex.test('/docs/dashboard')).toBe(true)
// A non-root matcher should not be widened to match the basePath root.
expect(regex.test('/docs')).toBe(false)
expect(regex.test('/docs/')).toBe(false)
})

it('does not match the bare basePath root for a param matcher', () => {
const regex = new RegExp(
getMiddlewareMatchers('/:id', {
i18n: undefined,
basePath: '/docs',
})[0].regexp
)

expect(regex.test('/docs/apple')).toBe(true)
expect(regex.test('/docs')).toBe(false)
})
})
})
31 changes: 28 additions & 3 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,19 @@ export function getMiddlewareMatchers(
}`
source = `${OPTIONAL_MIDDLEWARE_NEXT_DATA_PREFIX}${source}${sourceSuffix}`

// Whether this matcher (before prepending `basePath`) matches the site
// root `/`. A catch-all like `/((?!api).*)` matches `/` because its single
// optional capture group can be empty. We need to know this before adding
// `basePath` so we can keep the bare `basePath` root matching too (see
// below).
const matchesRoot =
!!nextConfig.basePath &&
!isRoot &&
(() => {
const { regexStr } = tryToParsePath(source)
return regexStr ? new RegExp(regexStr).test('/') : false
})()

if (nextConfig.basePath) {
source = `${nextConfig.basePath}${source}`
}
Expand All @@ -507,11 +520,23 @@ export function getMiddlewareMatchers(
process.exit(1)
}

// We know that parsed.regexStr is not undefined because we already
// checked that the source is valid.
let regexp = tryToParsePath(result.data).regexStr!

// When `basePath` is set, prepending it turns a matcher that previously
// matched `/` into one anchored at `${basePath}/...`, so the bare
// `basePath` root (e.g. `/docs`) no longer matches even though `/` did
// 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

regexp = `^(?:${basePathRoot}|${regexp.replace(/^\^/, '')})`
}

return {
...rest,
// We know that parsed.regexStr is not undefined because we already
// checked that the source is valid.
regexp: tryToParsePath(result.data).regexStr!,
regexp,
originalSource: originalSource || source,
}
})
Expand Down