Skip to content

fix(routing): return 404 for dynamic segments with un-decodable percent-encoded sequences#92557

Open
sleitor wants to merge 1 commit intovercel:canaryfrom
sleitor:fix-92527
Open

fix(routing): return 404 for dynamic segments with un-decodable percent-encoded sequences#92557
sleitor wants to merge 1 commit intovercel:canaryfrom
sleitor:fix-92527

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented Apr 9, 2026

What?

Dynamic routes like /foo/[slug] produce inconsistent errors when the URL contains a percent-encoded sequence that is not valid UTF-8 (e.g. /foo/%A0):

  • Development: HTTP 400 Bad Request
  • Production: HTTP 500 Internal Server Error

Why?

When route.match() in resolve-routes.ts is called for a dynamic route against a URL with an un-decodable segment, it calls decodeURIComponent('%A0') which throws a URIError. This was wrapped into a DecodeError in route-matcher.ts, but in certain production paths the error propagated unhandled, resulting in a 500.

More importantly, both 400 and 500 are wrong responses. A URL that cannot be decoded cannot match any real resource — the correct response is 404 Not Found.

How?

Catch DecodeError thrown by route.match() at both call sites in resolve-routes.ts:

  1. checkTrue() — the dynamic route loop used for filesystem-based route resolution. When a DecodeError is caught, continue to the next route, allowing the request to fall through to 404 handling.
  2. handleRoute() — the custom-route handler (headers, redirects, rewrites). When a DecodeError is caught, return (no match), so the loop continues.

A new e2e test (test/e2e/app-dir/dynamic-segment-invalid-encoding) covers both the broken case (%A0 → 404) and the valid case (%20 → 200).

Fixes #92527

…nt-encoded sequences

Previously, requests to dynamic routes with malformed percent-encoding
(e.g. /foo/%A0 where %A0 is not a valid UTF-8 sequence) produced
inconsistent responses: a 400 in development and a 500 in production.

Both are incorrect. An un-decodable URL cannot match any real resource,
so the correct response is 404 Not Found.

Fix: in resolve-routes.ts, catch DecodeError thrown by route.match()
in the dynamic route loop (checkTrue) and in the custom-route handler
(handleRoute). When the error occurs, skip the route instead of
propagating — this lets the request fall through to 404 handling.

Fixes vercel#92527
@nextjs-bot
Copy link
Copy Markdown
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: bba95a2

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dynamic segments with un-decodable entities → error 400 in dev, 500 in prod

2 participants