Next.js error
Attempted to Handle Request Too Many Times
The error
Attempted to handle request too many times /markets/spainA request is being rewritten or redirected back into something that rewrites it again. The loop is almost always in proxy.ts, and the URL in the message is the one going round.
Attempted to handle request too many times /markets/spain
Next.js counts how many times one incoming request has been handed back to the router. Past a small limit it stops and throws, because the alternative is serving nothing at all while the process spins.
The loop is nearly always in proxy.ts - the file that used to be called middleware.ts.
Read the URL as the destination, not the cause
The path in the message is the request that keeps arriving. In a rewrite loop that is the target, not the original. Somebody looking at /markets/spain and auditing the page component is reading the wrong end of it.
The question to ask is: what rewrites or redirects to this path, and does that same rule still match once it has.
The single-rule loop
A rule whose output satisfies its own condition will run forever:
// proxy.ts
export default function proxy(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/en')) {
const url = request.nextUrl.clone()
url.pathname = `/en${request.nextUrl.pathname}`
return NextResponse.rewrite(url) // now /en/x ... which still runs this
}
return NextResponse.next()
}That one terminates, because /en/x does start with /en. Invert the condition, add a locale the check does not know about, or rewrite to a path that a later rule sends back, and it does not.
The two-rule loop, where each rule is correct
The harder version. Rule A sends unprefixed paths to a locale prefix. Rule B sends a prefixed path for an English-only section back to the unprefixed form. Each is defensible on its own; together they are a cycle.
We built exactly this on our own site. Market pages exist in English only, so a German visitor requesting /markets/spain was redirected to /de/markets/spain, which did not exist, which was sent back. Two reasonable rules, one loop.
The fix was to stop one of them from applying rather than to add a third rule:
const PREFIXED_ENGLISH_ONLY = new RegExp(
`^/(${locales.filter((l) => l !== routing.defaultLocale).join('|')})/` +
`(${englishOnlyCollections.join('|')})(/.*)?$`, 'u',
)
export default function proxy(request: NextRequest) {
const match = PREFIXED_ENGLISH_ONLY.exec(request.nextUrl.pathname)
if (match) {
const url = request.nextUrl.clone()
url.pathname = `/${match[2]}${match[3] ?? ''}`
return NextResponse.redirect(url, 308) // ends here; no further rules run
}
return intl(request) // everything else
}Two things make it terminate. The redirect returns before the locale middleware runs, so the rule that would prefix it again never sees the request. And the destination cannot match PREFIXED_ENGLISH_ONLY, because it has no locale prefix left to match.
Finding it
Log the pathname at the top of the proxy and request the route once. A loop prints the same two or three paths alternating, and the alternation names both rules:
export default function proxy(request: NextRequest) {
console.log('[proxy]', request.nextUrl.pathname)
// …
}Then check the matcher export. A proxy that matches every path including /_next and static files runs on requests you never meant it to, and a rewrite that is harmless for a page can loop on an asset.
The two shapes that cause it
A rewrite whose destination still matches the condition that produced it. And a function with several early returns where one branch rewrites unconditionally, so a request that should have fallen through to NextResponse.next() never reaches it.
Loops in request handling are one of the things a technical audit looks for specifically, because they are invisible until a particular combination of path and header arrives - which, in the case above, meant any visitor whose browser asked for a language other than English.
