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.
curl -I http://localhost:3000/loginLeaving 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.
HTTP/1.1 307 Temporary Redirect
location: /loginThe 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.
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.
// 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.
// 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.
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.
Error: Proxy is missing expected function export nameThe 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
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.
Frequently Asked Questions
What does ERR_TOO_MANY_REDIRECTS mean?
How do I see a loop without the browser following it?
Why does my proxy file build fine but never execute?
Can a config redirect stop proxy from running?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.