How to Fix: Application Error with Middleware for Securing Protected Routes

7 min read

Your app crashes because the middleware is intercepting requests it should never touch, or it is trying to run authentication logic in a context that does not support the APIs being used. In Next.js, protected-route middleware must be extremely selective. If it matches static assets, internal framework paths, API handlers incorrectly, or performs invalid redirects, the result is often an Application Error, redirect loop, or a generic 500 response.

Understanding the Root Cause

This issue usually happens when a route protection middleware is configured too broadly or uses logic that is incompatible with the Edge Runtime. In Next.js, middleware runs before the request reaches your page or route handler, which makes it powerful but also easy to misconfigure.

The most common technical causes are:

  • Matcher is too broad: the middleware runs on /_next, image assets, favicon requests, static files, or even login pages, causing failures or redirect loops.
  • Redirect loop: unauthenticated users are redirected to /login, but the middleware also protects /login, so the request loops forever.
  • Using server-only or Node-only APIs in middleware: middleware runs in the Edge environment, so packages depending on full Node.js APIs can break.
  • Invalid cookie/session access: if the middleware expects a token or session shape that is missing, malformed, or only available in server components, it can throw before returning a response.
  • Protecting API and page routes with the same logic without exclusions: browser redirects make sense for pages, but can break API consumers expecting JSON.

In short, the error appears because the middleware is executing on the wrong requests or using authentication logic that does not safely handle all request types.

Step-by-Step Solution

The safest fix is to narrow the matcher, explicitly exclude public routes, and make the auth check resilient.

1. Protect only the routes that truly need protection

Do not apply middleware to your entire application unless absolutely necessary. Limit it to known protected paths such as /dashboard, /account, or /admin.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const PUBLIC_PATHS = ['/login', '/register', '/forgot-password']

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  const isPublicPath = PUBLIC_PATHS.some((path) => pathname.startsWith(path))

  if (isPublicPath) {
    return NextResponse.next()
  }

  const token = request.cookies.get('token')?.value

  const isProtectedPath =
    pathname.startsWith('/dashboard') ||
    pathname.startsWith('/account') ||
    pathname.startsWith('/admin')

  if (isProtectedPath && !token) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*', '/admin/:path*'],
}

2. Exclude framework internals and static files when using a broad matcher

If your application requires a broader matcher, explicitly skip internal and static requests.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  const isInternalPath =
    pathname.startsWith('/_next') ||
    pathname.startsWith('/api/public') ||
    pathname === '/favicon.ico' ||
    /\.(png|jpg|jpeg|gif|webp|svg|ico|css|js|map|txt)$/.test(pathname)

  if (isInternalPath) {
    return NextResponse.next()
  }

  const token = request.cookies.get('token')?.value

  if (!token && pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

3. Do not protect the login route with the same unauthenticated redirect logic

If /login is also matched by middleware and unauthenticated users are redirected to it, the app can enter a loop. Keep auth pages public.

const PUBLIC_PATHS = ['/login', '/register']

const isPublicPath = PUBLIC_PATHS.some((path) => request.nextUrl.pathname.startsWith(path))
if (isPublicPath) {
  return NextResponse.next()
}

4. Avoid Node-only libraries inside middleware

If you are validating a JWT or session in middleware, use an Edge-compatible approach. Some authentication libraries or custom token utilities rely on Node APIs and fail in middleware.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    // Replace this with an Edge-compatible token validation strategy
    // or keep middleware lightweight and validate deeply in route handlers.
    return NextResponse.next()
  } catch {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

5. Use different handling for page routes and API routes

For pages, redirecting to login is fine. For APIs, return a proper JSON error instead of redirecting.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const token = request.cookies.get('token')?.value

  if (!token && pathname.startsWith('/api/private')) {
    return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
  }

  if (!token && pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/private/:path*'],
}

6. Add safe debugging checks

When the issue is unclear, log only minimal request metadata and verify exactly which route triggers the failure.

export function middleware(request: NextRequest) {
  console.log('Middleware hit:', request.nextUrl.pathname)
  return NextResponse.next()
}

If every request logs, your matcher is too broad. If login logs and redirects repeatedly, you have a loop.

This pattern avoids the most common middleware failures for protected routes.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const PUBLIC_ROUTES = ['/login', '/register', '/forgot-password']
const PROTECTED_ROUTES = ['/dashboard', '/account', '/admin']

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  const isPublicRoute = PUBLIC_ROUTES.some((route) => pathname.startsWith(route))
  if (isPublicRoute) {
    return NextResponse.next()
  }

  const isProtectedRoute = PROTECTED_ROUTES.some((route) => pathname.startsWith(route))
  if (!isProtectedRoute) {
    return NextResponse.next()
  }

  const token = request.cookies.get('token')?.value

  if (!token) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*', '/admin/:path*'],
}

Common Edge Cases

  • Expired token: the cookie exists, but the token is invalid. If you only check presence, users may still reach protected pages until a deeper server-side validation fails.
  • Secure cookies in local development: cookies marked secure may not be sent over plain HTTP on localhost, making it look like authentication is broken.
  • Base path or i18n routes: if your app uses a base path or localized routes, your matcher may miss or wrongly include paths.
  • App Router vs Pages Router differences: route structures differ, so middleware patterns copied from another project may not match correctly.
  • Third-party auth helpers: some examples assume server components, route handlers, or API routes, but not middleware. Verify that the helper supports the Edge Runtime.
  • Redirecting authenticated users away from login: if you also redirect signed-in users from /login to /dashboard, make sure the logic does not conflict with your unauthenticated redirect flow.
  • Static asset breakage: if CSS, images, or scripts are intercepted by middleware, the page may render as a blank or broken application even though the root problem is route matching.

FAQ

Why does my app show an Application Error instead of redirecting to login?

Because the middleware is likely throwing before it can return NextResponse.redirect(). This often happens when using unsupported libraries, reading invalid session data, or matching requests like static assets and internal framework routes.

Why does protecting all routes with one matcher cause problems?

A global matcher also catches requests that should stay public or untouched, including /_next assets, auth pages, and special files. Middleware should be scoped narrowly unless you add strong exclusions.

Should I fully validate JWTs inside middleware?

Only if your validation method is Edge-compatible and lightweight. A common best practice is to do a quick presence check in middleware and perform full authorization in server-side handlers or protected pages.

For long-term stability, keep your middleware minimal: match only protected routes, exclude public and internal paths, avoid Node-specific code, and return the correct response type for pages versus APIs. That eliminates the majority of protected-route application errors in Next.js.

Leave a Reply

Your email address will not be published. Required fields are marked *