App Router Dynamic Segment With Malformed Percent-Encoding Returns 500 Instead Of 400
In App Router-only builds, requesting a dynamic route with a malformed percent-encoded segment (e.g. /items/%E0%A4) triggers a DecodeError during route matching. Base-server attempts to render the Pages Router /_error page with a 400 status, but that page does not exist in builds without any Pages Router routes, resulting in a bare 500 response.
When getRouteMatcher throws DecodeError for malformed percent-encoding, base-server catches it and calls renderError with status 400. renderError then tries to load the Pages Router error page /_error. In an App Router-only build, no pages/_error.js is generated (only 404.html and 500.html), causing the error rendering to fail with a 500.
1. Create a Next.js App Router project with a dynamic route app/items/[slug]/page.tsx and no pages directory.
2. Build and start the production server (npm run build && npm start).
3. Run: curl -o /dev/null -w '%{http_code}\n' http://localhost:3000/items/%E0%A4
4. Observe that the response code is 500 instead of the expected 400.
Fixing Code Block
// packages/next/src/server/base-server.ts
// Locate the catch block in the request handler and replace the DecodeError branch with:
if (err instanceof DecodeError) {
res.statusCode = 400
const body = 'Bad Request'
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.setHeader('Content-Length', String(Buffer.byteLength(body)))
await res.end(body)
return
}
Directly send a minimal 400 response when DecodeError is encountered, avoiding the dependency on the Pages Router /_error page. This ensures App Router-only builds return the correct HTTP status for malformed URLs without attempting to render a non-existent error page.
Edge Case Audit
This hotfix bypasses custom error handling for DecodeError: if a project deliberately provides a pages/_error.js to customize 400 responses for decode errors, that custom page will no longer be used. To avoid regressions, check for the existence of the error component before falling back to the plain response, or restrict the direct response to cases where no /_error page exists. The change is safe under concurrent requests because each request gets its own response object. Rollback: revert the patch and add a minimal pages/_error.js to the project as a workaround.