Debugging Redirect Loops and Proxy That Never Runs

Two failures that look like nothing is happening. Here is the single request that identifies a redirect loop, and the checklist for a proxy file that silently never executes.

9 min read

Debugging redirect loops and a proxy file that never runs are two different problems that feel identical from a browser. In one case nothing loads, in the other nothing happens, and in both the code looks correct.

The fastest way to tell them apart is a single request. A response that redirects to the path you just asked for is a loop, and a response missing a header you know the proxy sets means the function never ran.

bashbash
curl -I http://localhost:3000/login

Leaving off the redirect-following flag is the point. One request, one response, and the location header answers the question without the browser hiding the evidence behind a generic error page.

Reading the loop in one response

A loop announces itself when the location header names the same path as the request. Here is what a proxy file that redirects unauthenticated visitors to a login page returns for the login page itself.

texttext
HTTP/1.1 307 Temporary Redirect
location: /login

The request was for /login and the answer is to go to /login. The browser follows that a handful of times before giving up with an error about too many redirects, which is the browser's message rather than a Next.js one.

Once you can see this, the cause is usually obvious. The rule sends visitors without a session to the login page, and the login page also has no session, so the rule fires again.

Why loops form

Nearly every loop is the same mistake in a different costume. The destination of the redirect still satisfies the condition that produced the redirect.

  • An auth redirect whose destination is also covered by the matcher.
  • A locale redirect that adds a prefix the check does not then recognize.
  • A config redirect and a proxy rule that each undo the other's work.
  • A trailing slash rule fighting a redirect written in the other form.

The last two are harder to spot because no single file is wrong. Two rules are individually reasonable and collectively circular.

Two rules undoing each other

The diagram shows why reading either file alone finds nothing. The config rule is correct on its own, the proxy rule is correct on its own, and the routing order runs config redirects first every time.

Fixing a loop

Three fixes cover almost every case, and they are worth applying in this order.

Scope the matcher so it never covers the destination. This is the cleanest fix because it removes the possibility rather than guarding against it.

typescripttypescript
// proxy.ts
export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}

Listing the protected areas explicitly means the login page is simply not a path this function runs on. A broad negative pattern would have included it.

When a broad matcher is genuinely needed, guard on the pathname inside the function instead.

typescripttypescript
// proxy.ts
export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  if (pathname === '/login' || request.cookies.has('session')) {
    return NextResponse.next()
  }
  return NextResponse.redirect(new URL('/login', request.url))
}

The early return makes the destination reachable, which breaks the cycle. Requesting /login now returns the page, and requesting a protected path still redirects.

The third fix is to change the state that the condition reads. If the redirect exists to set something up, set it on the redirect response so the next request no longer matches.

When Proxy never runs and says nothing

The harder failure is silence. Some causes fail the build loudly, but the most common one does not fail at all.

Placement is that cause. The file has to sit at the project root, or inside src, at the same level as the app directory. A file at app/proxy.ts builds cleanly, produces no warning, and never executes, because at that path it is just an ordinary module nobody imports.

The build output tells you which happened. A registered proxy file appears as its own entry in the route listing, and its absence there means Next.js did not find one.

texttext
Route (app)
┌ ○ /
└ ○ /about
 
ƒ Proxy (Middleware)

That last line is the confirmation to look for. If it is missing while a proxy file exists in your repository, the file is in the wrong place or is not exporting what Next.js expects.

The loud failures

Three mistakes stop the build instead, which is the friendlier outcome. The first is exporting the wrong function name, which is easy to hit while migrating from the old convention.

texttext
Error: Proxy is missing expected function export name

The message goes on to list the likely causes, including a file that exports an object rather than a function, and a file still exporting a function named after the old middleware convention. Renaming the export to match the file fixes it.

The other two are covered elsewhere in this section. A route segment config export in the file fails because the runtime is fixed, and a matcher built from a variable fails because matcher values must be static.

When it runs but not where you expect

A proxy file can be registered, executing, and still appear dead on the path you are testing. Two causes account for most of it.

The matcher may not cover the path. This is worth checking with an actual request rather than by reading the pattern, since anchoring and the star modifier both surprise people.

The other cause is ordering. Config headers and config redirects are applied before the proxy file, so a matching config redirect answers the request and the function never sees it. That is documented behavior, not a bug, and it is a good reason to keep fixed path mappings in the config.

Add a temporary response header at the very top of the function while you investigate. If it appears on a path, the function ran there, and the problem is in the logic rather than in the wiring.

A diagnostic order that works

Work outward from the request rather than inward from the code, and each step rules out a whole category.

  • First, request the path with curl and no redirect following, and read the status and location.
  • Second, look for the proxy entry in the build output to confirm the file is registered.
  • Third, add a response header at the top of the function to confirm it runs for that path.
  • Fourth, check whether a config redirect matches the path and is answering first.
  • Last, read the branching logic inside the function.

Most of the time the answer arrives in the first two steps. A location header pointing back at the request is a loop, and a missing proxy entry in the build output is a file in the wrong place.

Rune AI

Rune AI

Key Insights

  • A single request with curl and no redirect following identifies a loop instantly.
  • Loops form when the redirect destination still satisfies the condition that caused the redirect.
  • A proxy file outside the project root builds successfully and silently never runs.
  • A wrong export name fails the build with a message naming the expected export.
  • Config redirects run before the proxy file, so a matching rule stops the function from seeing the request.
RunePowered by Rune AI

Frequently Asked Questions

What does ERR_TOO_MANY_REDIRECTS mean?

It is the browser giving up after following too many redirect responses in a row. The message comes from the browser, not from Next.js, so the cause is always in your rules rather than in the framework.

How do I see a loop without the browser following it?

Request the URL with curl and the head flag, without the location-following flag. A response whose location header points at the same path you requested is a loop in a single request.

Why does my proxy file build fine but never execute?

The most common cause is placement. The file has to sit at the project root, or inside src, at the same level as the app directory. Anywhere else it is treated as an ordinary module, so the build succeeds and nothing runs.

Can a config redirect stop proxy from running?

Yes. Config redirects are applied before the proxy file, so a matching rule answers the request and the proxy function never sees it. That is expected behavior rather than a bug.

Conclusion

Both problems are diagnosed by looking at one request rather than by reading code. A location header pointing at the path you requested is a loop, and a missing header you know the proxy sets means the file never ran. Check placement, export name, and matcher scope in that order before changing any logic.