Skip to content

Next.js error

Attempted to Handle Request Too Many Times

The error

Attempted to handle request too many times /markets/spain

A 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.

Related questions

Why does the message name a URL that looks fine?
Because it is the request that keeps coming back, not the one that is wrong. The URL you see is usually the destination of the rewrite rather than its source, so reading it as the cause sends you to the wrong file.
Does this happen in production too?
The loop does. The error may not: a hosting platform in front of your application can cut the request off with its own timeout or a 508 before Next counts high enough to throw. A loop that only shows as a slow route in production is the same bug.
Our proxy only has one rule. How can it loop?
One rule is enough if its output still matches its own condition. The rule runs again on the rewritten request, decides the same thing, and rewrites it again. Loops need two rules only when each is individually fine.
Is `NextResponse.next()` the safe default?
It is, and it is what every path through the function should end at. The dangerous shape is a function with several early returns where one of them rewrites unconditionally - the path that was supposed to fall through does not.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page