fix(middleware): align encoded path matching - #2802
Conversation
commit: |
|
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(middleware): align encoded path matching
I reviewed the diff, traced the new matcher logic through both entry points (server/prod-server.ts and server/app-middleware.ts), and re-ran the middleware suites locally. All 128 tests in middleware-runtime, middleware-runtime-trailing-slash, and middleware-matcher-auth pass, plus shims (matcher patterns), pages-data-*, and the App Router middleware integration tests (42). vp check is clean on both modified source files.
This is a solid, well-tested correctness fix. The core changes are sound:
Correct
- Evaluating the encoded pathname first, then a single full decode fallback, matches the Next.js
next-serverbehavior and the segment-decode approach that could turn%0A/%2Finto structurally different characters. The line-terminator (%0A,%0D,%E2%80%A8,%E2%80%A9) and delimiter (%2F,%3F,%23) cases are directly ported and exercised. - Dropping the
removeTrailingSlash(pattern)normalization inmatchPatternis the right call. The compiled regex already appends an optional terminal[/#?]?, so/api/admin/(literal trailing slash required) correctly no longer matches/api/admin. The updatedshims.test.tsandmiddleware-runtime-trailing-slash.test.tsexpectations reflect real path-to-regexp semantics rather than the previous artificial widening. - The
MiddlewareLocaleMatchContext(literal/internal/defaulted) cleanly encodes locale provenance so it is computed once and shared across all matchers in an array — the case-insensitive literal-locale detection stays consistent withstripLocalePrefix, andlocale: falsecorrectly bypasses locale handling. - Both entry points delegate to
executeMiddleware, so dev/prod/App Router/RSC parity is preserved./_next/staticand/_next/imagereturningfalseunder i18n matches Next.js (and/_next/datais normalized to the page path before this code runs, so it is unaffected).
Points worth confirming (not blockers)
-
Matcher now reads the raw request URL, not the caller-provided
normalizedPathname. The new logic derivesencodedRequestPathnamefromnew URL(options.request.url).pathnameand does its own basePath handling viaprepareMatcherPathname, whereas the old code matched againstbasePathStrippedPathname(derived fromnormalizedPathname). For App Router this shifts the source of truth fromcleanPathnameto the original request URL. ThehadBasePath === truebranch and the App Router integration tests cover this, but it is the highest-risk behavioral shift in the PR — worth a second look to confirm no caller relied on a pre-normalizednormalizedPathnamediverging from the raw URL (e.g. a caller that rewrote the pathname before invoking middleware).matchPathname/basePathStrippedPathnameis now used only for theNextFetchEventpage, no longer for eligibility. -
matchPatternroot special-case asymmetry. The guardif (pattern === "/" && (pathname === "//" || pathname === "/?" || pathname === "/#")) return false;blocks a bare terminal delimiter only for the root pattern, while/api/admin/?intentionally still matches/api/admin/. This is deliberate and test-backed (matchPattern("/?", "/")-> false vsmatchPattern("/api/admin/?", "/api/admin/")-> true), but a one-line comment explaining why root is special would help future readers. Real requests never reachmatchPatternwith////?(normalizePath collapses them), so this only affects the direct unit-test surface — fine. -
prepareMatcherPathnameendsWith("?")/endsWith("#")branch. This only triggers when a decoded candidate (decodeURIComponent) produces<basePath>?/<basePath>#;URL.pathnamenever contains those raw. The intent is subtle — the existing comment covers the encoded-basePath rationale, but explicitly noting these come from the decode fallback would aid readability.
Overall: correct, matches Next.js semantics, comprehensively tested, and preserves multi-runtime parity. Approving with the note on point #1 as the main thing to double-check.
|
Review posted to PR #2802. SummaryI reviewed PR #2802 ( Verdict: solid, well-tested correctness fix. The core changes are sound:
Non-blocking points flagged for the author:
No correctness defects or regressions found. |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(middleware): align encoded path matching
Re-reviewed the current head (d337861). I traced both entry points, re-ran the middleware suites (middleware-runtime, middleware-matcher-auth, middleware-runtime-trailing-slash, shims matcher patterns, pages-router encoded — all green), and confirmed vp check is clean on both source files. I verified the two highest-risk claims directly against Next.js canary source rather than trusting the comments:
Confirmed correct against Next.js source
-
Case-insensitive locale detection.
stripLocalePrefixand the runtime literal-locale detection now lowercase both sides. This matchesnormalizeLocalePathinpackages/next/src/shared/lib/i18n/normalize-locale-path.ts, which lowercases the segment and the locale list before comparing and returns the case-sensitive locale. The/EN/foo//Fr/foo→trueand/FR/aboutshim cases are correct. Note the runtime's own case-insensitivehasLiteralLocalecheck (middleware-runtime.ts:403-407) correctly pre-empts the still-case-sensitivenormalizeDefaultLocalePathname(pages-i18n.ts:92,locales.includes(parts[1])), so uppercase locales never fall into theinternal/defaultedclassification path — no divergence there, but that case-sensitivity gap innormalizeDefaultLocalePathnameis worth a follow-up since it is relied on indirectly. -
Trailing-slash source semantics. Dropping
removeTrailingSlash(pattern)inmatchPatternis correct:middlewarePathTokensToRegExp(middleware-path-to-regexp.ts:279) appends exactly[/#?]?$, somatcher: "/api/admin/"compiles to^/api/admin/[/#?]?$and correctly no longer matches/api/admin. The updatedshims.test.tsexpectations (matchesMiddleware("/api/admin", "/api/admin/")→false) reflect real path-to-regexp behavior, not the previous artificial widening. This is a deliberate behavioral change — any user whose matcher source carries a trailing slash while their requests don't will now see middleware stop firing. It matches Next.js, so it's the right call, but it's the kind of change that deserves a changelog/release note callout.
Other checks
- Encoded-first + single full-
decodeURIComponentfallback matchesnext-server; the line-terminator (%0A/%0D/%E2%80%A8/%E2%80%A9) and delimiter (%2F/%3F/%23) ports are exercised, and%5C/%252Fcorrectly do not overmatch. MiddlewareLocaleMatchContextis computed once and shared across array matchers;internal→ string matchers returnfalsewhilelocale:falsematchers still match internal paths (test-backed, matches Next.js).matchObjectMatcherdefaultedbranch inserts the domain-aware default locale forlocale:falsesources — thefr.example.comdomain test confirms domain-locale selection.- New optional
localeContextparam keepsmatchesMiddleware/matchPatternbackward-compatible for the only external caller (server/middleware.tsre-export).
One thing to double-check (non-blocking, same as prior review's point #1)
Eligibility now derives encodedRequestPathname from new URL(options.request.url).pathname (the raw request URL) and does its own basePath handling, while normalizedPathname/basePathStrippedPathname are now used only to build NextFetchEvent.page (middleware-runtime.ts:466). In app-middleware.ts the caller passes normalizedPathname: cleanPathname, and cleanPathname can be a rewritten pathname (app-middleware.ts:293) on the forwarded-context second pass. So on that path the eligibility check and the NextFetchEvent.page are now sourced from different values (raw URL vs rewritten clean path). The App Router integration tests pass, so this appears intended, but it's worth a sentence in the PR description confirming the raw-URL source of truth is desired for the forwarded-rewrite re-entry case.
Verdict: Correct, matches Next.js semantics, comprehensively tested, preserves multi-runtime parity. No correctness defects found. Approving with the trailing-slash behavior-change note (worth a release-note callout) and the forwarded-rewrite source-of-truth confirmation.
|
Review posted to PR #2802. SummaryI re-reviewed PR #2802 ( Verified correct against Next.js source:
Non-blocking items flagged:
No correctness defects or regressions found. Approving with notes. |
Summary
Validation