Fixing Next.js Middleware Redirect Infinite Loops
How to diagnose and resolve infinite redirect loops caused by Next.js Middleware configuration errors
Fixing Next.js Middleware Redirect Infinite Loops
Introduction
An infinite redirect loop in Next.js Middleware is one of the most disorienting bugs a developer can encounter. Your browser shows an error page, your server logs fill with identical redirect entries, and the root cause is often a subtle misconfiguration in the Middleware matcher or redirect logic.
I experienced this firsthand when implementing authentication middleware on a project. A single missing condition in the middleware caused the browser to bounce between /login and /dashboard endlessly. This post documents every pattern that causes infinite loops and how to fix each one.
Environment
- OS: Windows 11
- Node.js: v20.10.0
- Next.js: 14.0.4
- Authentication: Custom JWT middleware
Problem
The browser displayed:
This webpage has a redirect loop
ERR_TOO_MANY_REDIRECTSServer logs showed:
[Middleware] Redirect: /dashboard -> /login
[Middleware] Redirect: /login -> /login
[Middleware] Redirect: /login -> /login
[Middleware] Redirect: /login -> /login
(repeating indefinitely)Analysis
The five most common causes of infinite redirect loops:
1. Redirecting the target path. Redirecting from /dashboard to /login, but the middleware also applies to /login and redirects again.
2. Missing matcher exclusion. The middleware runs on every request including static files and API routes, causing unintended redirects.
3. Cookie not set before redirect. The middleware redirects to a page that should set a cookie, but the cookie is not yet available when the next request is made.
4. Conditional logic error. The if condition that decides whether to redirect is inverted or does not account for all cases.
5. Hostname mismatch. Redirect URL uses a different hostname than the current request, causing the browser to follow an external redirect.
Solution
Fix 1: Exclude the target path from redirect
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const session = request.cookies.get('session')
// IMPORTANT: Skip the login page itself
if (pathname === '/login' || pathname === '/api/auth/login') {
return NextResponse.next()
}
// Redirect to login if no session
if (!session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico (favicon)
* - public folder
* - api routes that handle their own auth
*/
'/((?!_next/static|_next/image|favicon.ico|public|api/auth).*)',
],
}Fix 2: Use the matcher to exclude paths
// middleware.ts
export const config = {
matcher: [
// Only match these paths
'/dashboard/:path*',
'/settings/:path*',
'/profile/:path*',
// Explicitly exclude these
'/((?!_next|api|login|register|forgot-password).*)',
],
}Fix 3: Handle the redirect chain properly
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const session = request.cookies.get('session')
// Paths that don't require authentication
const publicPaths = ['/login', '/register', '/forgot-password', '/api/auth']
const isPublicPath = publicPaths.some((path) => pathname.startsWith(path))
// Already authenticated - redirect away from login
if (session && isPublicPath) {
const from = request.nextUrl.searchParams.get('from') || '/dashboard'
return NextResponse.redirect(new URL(from, request.url))
}
// Not authenticated - redirect to login (but not if already on login)
if (!session && !isPublicPath) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}Fix 4: Handle edge cases with URL comparison
// middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const response = NextResponse.next()
// Compare against the canonical URL to prevent redirect loops
const canonicalPath = pathname.replace(/\/+$/, '') || '/'
if (canonicalPath === '/login') {
// Don't redirect from login
return response
}
// ... redirect logic
return response
}Lessons Learned
Always test redirect middleware with cookies cleared. Cached cookies can mask redirect loop issues during development.
Use the Network tab to see redirect chains. Chrome DevTools shows the full sequence of 302 redirects, making it easy to identify the loop.
Add logging before redirects. Log the current path, the target path, and the condition that triggered the redirect:
console.log(`[Middleware] ${pathname} -> ${redirectPath} (${reason})`)Test with both authenticated and unauthenticated states. Many redirect loops only appear in one state.
Use
return NextResponse.next()for non-matching paths rather than redirecting. Only redirect when you have a specific reason.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.