Skip to content

fix(config): match redirects, rewrites and headers sources with path-to-regexp - #4062

Open
james-elicx wants to merge 57 commits into
mainfrom
fix/config-path-to-regexp
Open

james-elicx wants to merge 57 commits into
mainfrom
fix/config-path-to-regexp

Conversation

@james-elicx

@james-elicx james-elicx commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

next.config.js redirects, rewrites and headers sources were matched by a hand-rolled tokenizer (plus a separate escapeHeaderSource for headers), which diverged from Next.js in many shapes. This PR compiles them the way Next.js 16 does at runtime instead: path-to-regexp 6 (strict: true, delimiter: "/", case-insensitive) plus modifyRouteRegex's optional trailing slash, using the path-to-regexp port vinext already vendors for middleware matchers. Custom param patterns go through the same per-token ReDoS validation middleware matchers use.

Supersedes #4029, which fixed one of these divergences by hand.

Fixed divergences (all checked against Next's own compiled path-to-regexp / getPathMatch)

  • Parameterized locale-prefixed redirects (/:locale(en|sv)?/settings/:path*, and the /:nextInternalLocale(...)/... variants i18n generates) never fired under non-default locales: the redirect locale index swallowed sources whose suffix contained params.
  • /x/:locale? and /:locale(en|sv)?/x/:path* required the slash before an absent optional param.
  • Unnamed groups (/foo/(bar|baz)/:id) leaked into params.
  • . as a param prefix (/files/:name{.:ext}?, /user/:id.:format), {} groups, (\d+)* repeats and /:path*.md vs /.md now follow path-to-regexp.
  • :auth-method is the auth param followed by literal -method, as in path-to-regexp's name lexer.
  • Header sources use the same compiler, so a glob like /api/* is now an invalid source (see Behaviour change).
  • Trailing slash: one optional trailing slash, exactly as Next's regex (/docs/:rest(.*) on /docs/a/ captures a/, like Next).
  • Sources path-to-regexp rejects, and custom patterns that are not valid JS regexes, fail config loading (see Behaviour change) instead of matching unpredictably or throwing per request.
  • Rewrite query append skips optional params that did not match (Next leaves them out of params), while an empty capture is still appended.

The redirect locale index now only indexes sources whose result is identical to the compiled source (literal-only suffix, no trailing slash, \w param name, plain-word alternatives).

Behaviour change: invalid sources fail config loading

Like Next.js's checkCustomRoutes, which fails the build, resolveNextConfig now throws Invalid next.config.js <type> source(s) and lists every offending source. A source is invalid when it:

  • is missing, is not a string, or does not start with /;
  • is rejected by path-to-regexp (e.g. a header glob such as /api/*);
  • compiles to a regex over 4,096 characters, checked again after the basePath and i18n locale prefixes are added, as Next does.

Previously vinext skipped such sources at runtime, so an app that has one will now fail to start or build until the source is fixed. Sources that are valid but rejected by the conservative ReDoS check below are not load errors, because Next accepts them: they are skipped at runtime, and vinext warns about each one once when the config loads (documented in docs/reference/differences.mdx).

ReDoS validation

Each param's custom pattern first goes through the existing per-pattern structural checks (nested repetition, ambiguous alternatives under repetition). The compiled source (and each middleware matcher) is then checked for exponential backtracking with an exact ambiguity test, which replaces the old whole-source repeated-alternative heuristics. It builds the regex's position (Glushkov) automaton, counting paths per edge like the backtracking engine and modelling the (?!prefix) guards path-to-regexp adds before repeated params. It then looks for a strongly connected component of the automaton's self-product with both a diagonal and an off-diagonal state, or a parallel edge (Allauzen–Mohri–Rastogi). Repeats are unrolled exactly while the automaton fits a 16,384-position budget; past that, repeats over 1,024 positions are read as loops, which can only add ambiguity. Construction, guard expansion and the product search each have a work budget, and a source over budget is rejected as source exceeds the regex analysis budget.

The Pages client's synchronous beforeFiles rewrite fast path keeps config-matchers out of the eager router bundle, so it stays a small matcher, but now only models whole-segment literals, :name and a final :name* / :name+ (ASCII case-insensitive), and defers every other shape to the exact async matcher.

Known limitations (unchanged)

  • Repeated params are the captured string (a/b) rather than Next's arrays, and a missing optional param substitutes as "".
  • Redirects do not get Next's ^(?!/_next) lookahead; caseSensitiveRoutes is not supported.
  • The per-pattern structural checks are conservative, so some linear params are rejected as nested repetition, such as the slug source /blog/:slug([a-z0-9]+(?:-[a-z0-9]+)*) (documented in differences.mdx). fix(regex-safety): measure polynomial ambiguity exactly instead of guessing from nesting #4069 replaces them with the exact analysis, which accepts it.
  • The analysis budget is a conservative fallback, so some linear sources are rejected: a repeated token with a long self-overlapping literal prefix (/{aaaa…:id}+ with ~96+ identical characters, or a ~1,000-character periodic prefix), and repeats whose exact unrolling exceeds the position budget (/{a:id(a{16000})}+X).
  • The client fast path's prefilter only proves a non-match from the literal text before a rule's first param (plus the character after a skippable optional param). A conditional or unsupported beforeFiles rule whose later segments rule out the pathname, such as /foo/:id?/bar on /foo/baz, still sends that navigation to the async matcher, as on main.
  • The analysis ships in the lazily loaded client rewrite matcher, which adds about 3% (4.3 KB) to the gzipped client bundle and about 2% to the server bundle.
  • A backreference is modelled as one of its group's fixed words, or as a run of the characters its group reads, but each occurrence on its own. A repeat of one backreference to a group with overlapping alternatives, such as /:a(a|aa)/:id(\\1+), is therefore rejected even though the engine compares one captured string.
  • The per-token ReDoS validation does not yet detect polynomial backtracking across several params (e.g. /:a(.*)-:b(.*)); that is handled in a follow-up PR stacked on this one.

Tests

  • tests/config-source-matching.test.ts:
    • a differential test over ~50 sources × ~80 pathnames against Next's getPathMatch + modifyRouteRegex;
    • named regression tests for each divergence;
    • locale index ⇔ compiled source agreement;
    • client fast path ⇔ server matcher agreement;
    • ReDoS tests: 16 KB near-miss pathnames on the structural shapes, and rejection of nested or overlapping custom patterns.
  • tests/e2e/pages-router/i18n-locale-case-config.spec.ts: parameterized locale-prefixed redirects on the checked-in pages-i18n-public-rewrite fixture, with expectations observed on real Next.js 16.2.7.
  • Middleware matchers share the ReDoS check, so the old rule that rejected any repeated param able to match empty or / is replaced by the exact ambiguity analysis: /:p([a-z]*)+ and similar now compile, while /:p(.*)+ and /:path(.*)*/end are still rejected (tests updated for the new messages). A middleware matcher the check rejects still fails, as before.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 1d6b1d250350e9c83c9c0df83f3d30b5d9936d46 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@pkg-pr-new

pkg-pr-new Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@vinext/cloudflare

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

create-vinext-app

npm i https://pkg.pr.new/create-vinext-app@4062

@vinext/types

npm i https://pkg.pr.new/@vinext/types@4062

vinext

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

@cloudflare/workers-response-store

npm i https://pkg.pr.new/@cloudflare/workers-response-store@4062

commit: d16d58b

@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
response-store-demo preview production
workers-cache preview production
kv preview production
static-assets-cache preview production
static-assets-pages preview production
static-assets-pages-i18n preview production
static-export preview production
web preview production

Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/config/config-matchers.ts Outdated
Comment thread packages/vinext/src/client/client-config-source-match.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 3 findings.

Posted three inline findings: one blocking ReDoS issue and two matcher parity bugs.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 4608c78b50261b35bbe071ed5ebabc27282aeb06 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared d16d58b against base 218790e using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 1 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 154.2 KB 159.8 KB 🔴 +3.6%
Client entry size (gzip) vinext 141.7 KB 141.7 KB ⚫ +0.0%
Dev server cold start vinext 2.17 s 2.17 s ⚫ -0.2%
Dev server cold start (large Pages Router app) vinext 18.61 s 18.47 s ⚫ -0.8%
Production build time vinext 2.59 s 2.56 s ⚫ -1.2%
RSC entry closure size (gzip) vinext 142.0 KB 142.0 KB ⚫ -0.0%
Server bundle size (gzip) vinext 250.7 KB 254.3 KB ⚫ +1.5%

View detailed results and traces

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

Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/shims/router.ts
Comment thread packages/vinext/src/config/config-matchers.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 5 findings.

Posted five inline findings: one blocking ReDoS bypass and four matcher/config parity defects. Reviewed exact head 4608c78; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 1677a629367bc3228175f5fa5646b77c1e76e00f without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow timed out.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head c88d6cfe76416a810577888ed8afba3a99fd1be1 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head c88d6cfe76416a810577888ed8afba3a99fd1be1 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

Comment thread packages/vinext/src/client/client-config-source-match.ts
Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 2 findings.

Posted two inline warnings on exact head c88d6cfe. Branch unchanged. Targeted matcher tests pass.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 350811626eb6d8cc679fbb0ddc828c69367a0838 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

Comment thread packages/vinext/src/config/next-config.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 1 findings.

Posted one inline warning on source validation parity. Targeted tests pass; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head bb369c2ac09e72318e6bed2f5fb3be4f5d82699d without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

Comment thread packages/vinext/src/config/config-matchers.ts
Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/config/next-config.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 3 findings.

Posted three inline findings: one blocking ReDoS regression and two matcher/config parity defects. Targeted tests pass; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 6d9e3ac4baf657aba3e5032584a0222dc1ac5dad without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/client/client-config-source-match.ts
Comment thread packages/vinext/src/shims/router.ts
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 3 findings.

Posted three inline findings: one blocking ReDoS bypass and two client matcher parity defects. Reviewed exact head 6d9e3ac; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 7486fecb23210455635030a8f787ba5012b4f3f7 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/utils/path-token-safety.ts Outdated
Comment thread packages/vinext/src/config/next-config.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Review: 4 findings.

Posted four inline findings: one blocking ReDoS bypass and three matcher/config parity defects. Targeted tests pass; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 733222c1ee5152165184aabaff00f1dcfb2f0bbb without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 806867137fff0a1bd7b2534d10d41521aa4a93ba without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 806867137fff0a1bd7b2534d10d41521aa4a93ba without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head aacf25c0b2b84f53f4e6892bea6df2166a227bd5 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

Comment thread packages/vinext/src/client/client-config-source-match.ts
@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Review: 1 finding.

Posted one inline warning for optional unnamed groups bypassing client redirect/rewrite matching. Targeted tests pass; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head b720834998c1cb9dee91d5c821dfbd8ce092d8a3 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk, on the optional unnamed group finding: I couldn't reproduce it on aacf25c. clientConfigSourceCouldMatch("/foo", "/foo/(bar)?") returns true. When the character after the literal / or . is not a :name, end stays -1, so the end === -1 branch drops that optional prefix and the required prefix becomes /foo. The same holds for /foo/(bar)*, /foo.(bar)? and /foo.(bar)*. matchSimpleClientConfigSource then returns undefined for the unnamed-group segment, which clientConfigRedirectCouldMatch treats as a possible match (params !== null), so the async matcher still runs. b720834 adds these sources, plus /foo/(bar)?/x, to the cross-product test against matchConfigPattern and to the explicit pre-check and defer cases. They pass unchanged.

@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

Comment thread packages/vinext/src/config/next-config.ts Outdated
@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Review: 1 findings.

Posted one inline warning about rewrite validation running after unsafe dereferences. Reviewed exact b720834; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head a094f42ffb325748bd98616c3ae314efdb3d1a88 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk, on rewrite validation running after field reads: confirmed and fixed in a094f42. Redirect and rewrite sources are now validated before the basePath: false and external-rewrite checks read their fields, keeping Next's redirect, rewrite, header order. rewrites: () => [null] now fails with "Invalid next.config.js rewrite source" instead of a TypeError. The missing-entry test covers redirects, rewrites (array and beforeFiles) and headers, and its rewrite case fails on the previous head with your TypeError.

@clanky-the-clanker, on the weak skip test: fixed in 0b9823a. "leaves ReDoS-unsafe sources to warn and skip when matched" now also asserts the (ReDoS risk) warning at config load and that matchHeaders returns no headers for a matching path.

@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Review: 1 findings.

  • Warning: packages/vinext/src/config/next-config.ts:1725 calls JSON.stringify(route?.source) while formatting invalid routes. A source of 1n throws TypeError: Do not know how to serialize a BigInt, bypassing the intended Invalid next.config.js ... source diagnostic. Use a safe formatter for arbitrary config values.

Targeted tests passed: tests/config-source-matching.test.ts and relevant tests/shims.test.ts cases.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 0f0e0b7e3c0508a10f9c89484f53ff9bed443085 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk, on the unserializable source: confirmed and fixed in 0f0e0b7. The invalid-source message now quotes a string source as before and shows any other value by its type, for example (bigint), so formatting it can't throw. The missing-entry test now also covers 1n, a symbol, a null-prototype object and an object whose toJSON throws. The 1n case fails on the previous head with your TypeError.

@clanky-the-clanker, on the execConfigSource allocations: I'm leaving them. They only happen after a source has matched, and a small Set and key array for a few params cost little next to the regex match and the rest of the request.

@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 0f0e0b7e3c0508a10f9c89484f53ff9bed443085 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

…ub.com/cloudflare/vinext into fix/config-path-to-regexp

# Conflicts:
#	packages/vinext/src/config/config-matchers.ts
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head ab3a9b662e80860286299f1568f4774dad8531ed without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Review: 1 findings.

  • Warning packages/vinext/src/client/client-config-source-match.ts:179: /post-:id is considered viable for /post-123/extra, although the exact matcher rejects it. This forces asynchronous rewrite resolution and regresses same-turn history updates for later matching rewrites. Restore the segment-boundary check for this unsupported partial-segment shape.

Targeted matcher tests pass; branch unchanged.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Re the clanky warning about /blog/:slug([a-z0-9]+(?:-[a-z0-9]+)*) being rejected: that's a known limitation of this PR's structural nested-repetition check, and it's documented in differences.mdx. The stacked follow-up #4069 replaces that check with the exact automaton analysis, which accepts this slug source (and /:v(\d+(?:\.\d+)*)) while still rejecting genuinely ambiguous params such as /:x((?:a+)+).

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head c40a32028677ddd8f587eaa4694c42fef0e057cb without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head d16d58b3fa5bcd4304e5201fc387e242a8ebec2d without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

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