Summary
Routes compiled from next.config.ts's redirects() / rewrites() (and any other manifest-driven route matching) lose case-insensitivity when matched at runtime in the OpenNext-built bundle, even though the equivalent request is correctly matched case-insensitively by next dev / next start.
Root cause: every runtime route-matching call site in @opennextjs/aws constructs new RegExp(pattern) with no flags against routes-manifest.json entries. Next.js's own manifest-driven matching defaults to case-insensitive (caseSensitiveRoutes: false unless a project opts in), but nothing in @opennextjs/aws's matcher passes an i flag (or otherwise respects the manifest's case-sensitivity setting) when reconstructing these regexes.
Environment
@opennextjs/aws: 4.0.2 (confirmed still present in 4.1.0, the latest published version as of this report)
@opennextjs/cloudflare: 1.19.11
next: 16.2.6
- Deployed target: Cloudflare Workers
Repro
- In
next.config.ts, add a redirect:
async redirects() {
return [
{ source: '/mypartner', destination: '/learn/partners/mypartner', permanent: true },
];
}
- Run
next dev and request /MyPartner (or /MYPARTNER) → correctly redirects to /learn/partners/mypartner. Next's dev-time route matching is case-insensitive by default.
- Build with
@opennextjs/cloudflare and deploy to Workers (or run the built Worker locally via wrangler dev).
- Request
/MyPartner (or /MYPARTNER) against the deployed Worker → 404. Only the exact-case /mypartner redirects correctly.
Root cause detail
Confirmed directly against the published @opennextjs/aws@4.1.0 npm package (dist/) — every runtime route-regex construction site builds the RegExp with no flags argument:
dist/core/util.js:110 — new RegExp(route.regex).test(rawPath ?? "")
dist/core/routing/matcher.js:80 — new RegExp(regex).test(path)
dist/core/routing/matcher.js:112 — new RegExp(route.regex).test(path)
dist/core/routing/matcher.js:304 — new RegExp(routeRegex)
dist/core/edgeFunctionHandler.js:17 — new RegExp(r) (per-route regex from globalThis._ROUTES)
dist/core/routing/cacheInterceptor.js:215 — new RegExp(dr.routeRegex).test(localizedPath)
None of these read or forward a case-sensitivity flag from the corresponding routes-manifest.json entry (or from next.config.ts's caseSensitiveRoutes setting). Compare with the flag-aware helper already present elsewhere in the same package, dist/utils/regex.js's getCrossPlatformPathRegex(regex, { flags = "" }) — which demonstrates the codebase already has a pattern for this, just not applied at the call sites above.
I checked the v4.0.2 → v4.1.0 diff on matcher.ts, cacheInterceptor.ts, and edgeFunctionHandler.ts (files that did change between those versions) — the changes are unrelated (atomic decodeURIComponent / malformed-path decode-safety, and an added decoded-path match variant in edgeFunctionHandler). None of them add a case-insensitivity flag.
Impact
Any project relying on next.config.ts's built-in redirects()/rewrites() case-insensitive matching (the Next.js default) will see that behavior silently work in next dev but break in the OpenNext/Cloudflare Workers build — no build-time warning, no type error, just a 404 in production for any mixed-case request that isn't an exact match. We only caught this because we noticed vanity-path redirects behaving inconsistently between local dev and the deployed Worker.
Workaround in use
We added an explicit case-folding layer in our own middleware.ts (lowercase the incoming path segment, look it up against a pre-extracted lowercase table, rewrite/redirect to the canonical lowercase path) to route around this, since we can't patch the vendored matcher. Happy to share the pattern if useful, but it only covers our own redirect table — any other next.config.ts project would hit the same bug with zero workaround unless they build the same kind of shim.
Suggested fix
Thread the manifest's case-sensitivity setting (or default to case-insensitive, matching Next's own default) through to each of the new RegExp(...) call sites listed above, the same way getCrossPlatformPathRegex already supports a flags option.
Summary
Routes compiled from
next.config.ts'sredirects()/rewrites()(and any other manifest-driven route matching) lose case-insensitivity when matched at runtime in the OpenNext-built bundle, even though the equivalent request is correctly matched case-insensitively bynext dev/next start.Root cause: every runtime route-matching call site in
@opennextjs/awsconstructsnew RegExp(pattern)with no flags againstroutes-manifest.jsonentries. Next.js's own manifest-driven matching defaults to case-insensitive (caseSensitiveRoutes: falseunless a project opts in), but nothing in@opennextjs/aws's matcher passes aniflag (or otherwise respects the manifest's case-sensitivity setting) when reconstructing these regexes.Environment
@opennextjs/aws: 4.0.2 (confirmed still present in 4.1.0, the latest published version as of this report)@opennextjs/cloudflare: 1.19.11next: 16.2.6Repro
next.config.ts, add a redirect:next devand request/MyPartner(or/MYPARTNER) → correctly redirects to/learn/partners/mypartner. Next's dev-time route matching is case-insensitive by default.@opennextjs/cloudflareand deploy to Workers (or run the built Worker locally viawrangler dev)./MyPartner(or/MYPARTNER) against the deployed Worker → 404. Only the exact-case/mypartnerredirects correctly.Root cause detail
Confirmed directly against the published
@opennextjs/aws@4.1.0npm package (dist/) — every runtime route-regex construction site builds theRegExpwith no flags argument:dist/core/util.js:110—new RegExp(route.regex).test(rawPath ?? "")dist/core/routing/matcher.js:80—new RegExp(regex).test(path)dist/core/routing/matcher.js:112—new RegExp(route.regex).test(path)dist/core/routing/matcher.js:304—new RegExp(routeRegex)dist/core/edgeFunctionHandler.js:17—new RegExp(r)(per-route regex fromglobalThis._ROUTES)dist/core/routing/cacheInterceptor.js:215—new RegExp(dr.routeRegex).test(localizedPath)None of these read or forward a case-sensitivity flag from the corresponding
routes-manifest.jsonentry (or fromnext.config.ts'scaseSensitiveRoutessetting). Compare with the flag-aware helper already present elsewhere in the same package,dist/utils/regex.js'sgetCrossPlatformPathRegex(regex, { flags = "" })— which demonstrates the codebase already has a pattern for this, just not applied at the call sites above.I checked the v4.0.2 → v4.1.0 diff on
matcher.ts,cacheInterceptor.ts, andedgeFunctionHandler.ts(files that did change between those versions) — the changes are unrelated (atomicdecodeURIComponent/ malformed-path decode-safety, and an added decoded-path match variant inedgeFunctionHandler). None of them add a case-insensitivity flag.Impact
Any project relying on
next.config.ts's built-inredirects()/rewrites()case-insensitive matching (the Next.js default) will see that behavior silently work innext devbut break in the OpenNext/Cloudflare Workers build — no build-time warning, no type error, just a 404 in production for any mixed-case request that isn't an exact match. We only caught this because we noticed vanity-path redirects behaving inconsistently between local dev and the deployed Worker.Workaround in use
We added an explicit case-folding layer in our own
middleware.ts(lowercase the incoming path segment, look it up against a pre-extracted lowercase table, rewrite/redirect to the canonical lowercase path) to route around this, since we can't patch the vendored matcher. Happy to share the pattern if useful, but it only covers our own redirect table — any othernext.config.tsproject would hit the same bug with zero workaround unless they build the same kind of shim.Suggested fix
Thread the manifest's case-sensitivity setting (or default to case-insensitive, matching Next's own default) through to each of the
new RegExp(...)call sites listed above, the same waygetCrossPlatformPathRegexalready supports aflagsoption.