Fix Next.js Middleware Redirect Loop: Root Causes and Minimal Checks
Quick Answer
- Conclusion: Middleware redirect loops occur when the redirect condition is met even after the redirect has already been applied, causing the browser to cycle indefinitely. The fix is to add an exclusion check for the target path before redirecting.
- First checks: Verify that your
middleware.tsfile is in the project root (same level aspages/orapp/, or insidesrc/), and that yourmatcherconfiguration is a static constant array with paths starting with/. - Minimal fix: Before calling
NextResponse.redirect(), checkrequest.nextUrl.pathnameto ensure the current path is not already the redirect destination. For example,if (request.nextUrl.pathname === '/home') return NextResponse.next(). - Applicable environment: Next.js 14 Pages Router (not App Router). Middleware runs on Edge Runtime, not Node.js.
What Problem It Solves
Next.js Middleware allows you to run code before a request completes, enabling authentication checks, redirects, rewrites, A/B testing, internationalization (i18n), and logging. However, a common pitfall is creating an infinite redirect loop where the Middleware redirects a request to a path, and then the same Middleware logic redirects that path again, causing the browser to report ERR_TOO_MANY_REDIRECTS. This article explains how to identify, debug, and fix redirect loops in Next.js 14 Pages Router Middleware.
When This Error or Setup Appears
The redirect loop error typically appears in these scenarios:
- Authentication redirects: Redirecting unauthenticated users to a login page, but the login page itself triggers the same redirect condition.
- i18n redirects: Redirecting users based on
Accept-Languageheader, but the redirected path still matches the redirect rule. - Role-based routing: Redirecting users to different dashboards based on their role, but the dashboard path itself triggers another redirect.
- Custom domain or subdomain handling: Redirecting based on hostname, but the target hostname also matches the redirect condition.
Minimal Working Configuration
Place this middleware.ts file in your project root (same level as pages/ or inside src/):
TYPESCRIPT// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // Check if the user is authenticated (example: check for a cookie) const isAuthenticated = request.cookies.has('session'); const { pathname } = request.nextUrl; // CRITICAL: Prevent redirect loop by checking the target path if (pathname === '/login') { return NextResponse.next(); } if (!isAuthenticated) { const loginUrl = new URL('/login', request.url); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
The matcher above excludes API routes, static assets, and favicon from Middleware execution. Adjust it to match your project's needs.
Root Cause Analysis
A redirect loop occurs because the Middleware function runs on every request that matches the matcher configuration. When you redirect to a path (e.g., /login), the browser makes a new request to that path. If the Middleware does not exclude that path from the redirect logic, it will redirect again, creating an infinite cycle.
Common patterns that cause loops:
| Pattern | Problem | Fix |
|---|---|---|
Redirect all unauthenticated users to /login | The /login page itself is unauthenticated, so it redirects to itself | Check pathname !== '/login' before redirecting |
| Redirect based on cookie value | The redirected path also matches the cookie-based condition | Add an early return for the target path |
| Redirect based on hostname | The target hostname also triggers the same redirect | Use a different condition or exclude the target hostname |
Execution order matters: next.config.js redirects run before Middleware. If a static redirect in next.config.js conflicts with your Middleware logic, the static redirect takes precedence. Middleware will not execute for paths already handled by redirects.
Common Errors and Fixes
Error 1: Redirect Loop (ERR_TOO_MANY_REDIRECTS)
Solution: Add a guard clause that returns NextResponse.next() for the redirect destination path before the redirect logic.
TYPESCRIPT// BAD - causes loop if (!isAuthenticated) { return NextResponse.redirect(new URL('/login', request.url)); } // GOOD - prevents loop if (pathname === '/login') { return NextResponse.next(); } if (!isAuthenticated) { return NextResponse.redirect(new URL('/login', request.url)); }
Error 2: Middleware Not Executing
Solution: Verify these conditions:
middleware.tsis in the project root (not insidepages/orapp/)matcherpaths start with/and are static constants (no variables)- Clear Next.js cache: delete the
.nextdirectory and rebuild
BASHrm -rf .next npm run build
Error 3: TypeError: Cannot read properties of undefined (reading 'get')
Solution: Ensure the request parameter is typed correctly. In TypeScript, import NextRequest from next/server:
TYPESCRIPTimport type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // Now request.cookies and request.nextUrl are properly typed }
Error 4: Edge Runtime Node.js API Errors
Solution: Middleware runs on Edge Runtime, which does not support Node.js native modules (fs, path, crypto with Node.js API, Buffer). Move any Node.js-dependent logic to API routes or getServerSideProps. Use Web APIs instead:
- Use
fetch()instead offs.readFile() - Use
TextEncoder/TextDecoderinstead ofBuffer - Use Web Crypto API instead of
cryptomodule
Production Notes and Security Checks
Critical Limitations
| Limitation | Impact | Workaround |
|---|---|---|
| Edge Runtime only | No fs, path, Buffer, or Node.js crypto | Move Node.js logic to API routes |
matcher must be static | Cannot use dynamic values or environment variables | Define all paths statically in the array |
| Request header size limit | Large cookies or custom headers cause 431 errors | Keep headers under 8KB |
Execution order with redirects | Static redirects in next.config.js run first | Exclude conflicting paths from redirects |
Security Best Practices
- Never expose API keys or secrets in Middleware code. Use environment variables only in server-side code (API routes,
getServerSideProps). - Validate authentication tokens securely using Web Crypto API or a lightweight JWT library that supports Edge Runtime (e.g.,
jose). - Avoid expensive operations in Middleware. Keep execution time under 50ms to prevent request delays.
- Use
waitUntilfor background tasks (Next.js 14+):event.waitUntil(promise)allows non-blocking operations like logging or analytics without delaying the response.
Debugging Tips
- Add temporary logging to trace path changes:
TYPESCRIPT
console.log('Middleware path:', request.nextUrl.pathname); - Use
NextResponse.rewrite()instead ofredirect()during development to avoid browser redirect loops while testing. - Check the browser's Network tab for the redirect chain to identify which paths are involved.
FAQ
Q: How do I implement role-based redirects in Middleware?
A: Parse the JWT token or session cookie to extract the user's role. Use Web APIs (not jsonwebtoken) for decoding. Example:
TYPESCRIPTconst token = request.cookies.get('token')?.value; if (token) { const payload = JSON.parse(atob(token.split('.')[1])); // Decode JWT payload if (payload.role === 'admin' && pathname.startsWith('/dashboard/user')) { return NextResponse.redirect(new URL('/dashboard/admin', request.url)); } }
Q: What is the priority between Middleware and next.config.js redirects?
A: next.config.js redirects execute before Middleware. If a static redirect matches a path, Middleware will not run for that path. To have Middleware override redirects, exclude the relevant paths from redirects or handle all logic in Middleware.
Q: How do I handle i18n redirects without causing loops?
A: Check if the path already contains a language prefix before redirecting:
TYPESCRIPTconst hasLangPrefix = /^\/(en|fr|de|ja)/.test(pathname); if (!hasLangPrefix) { const lang = request.headers.get('accept-language')?.split(',')[0]?.split('-')[0] || 'en'; return NextResponse.redirect(new URL(`/${lang}${pathname}`, request.url)); }