DEV Community

Cover image for Fix: Next.js middleware not triggering
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix: Next.js middleware not triggering

middleware.ts running with zero console output and zero effect on requests
has three unrelated causes, and they fail silently in the same way — no error,
no warning, the request just passes through as if the file did not exist.
Check them in this order: file location, matcher config, then runtime
compatibility. Each one rules out roughly a third of real cases.

Check 1: the file is in the wrong place

Middleware only runs from a single, specific locationmiddleware.ts
(or .js) directly inside src/ if you use a src directory, otherwise
directly inside the project root, next to package.json. It is not
matched anywhere else, including app/middleware.ts or a nested folder:

✅ my-app/middleware.ts             (no src dir)
✅ my-app/src/middleware.ts         (with src dir)
❌ my-app/app/middleware.ts         (never runs)
❌ my-app/src/app/middleware.ts     (never runs)
Enter fullscreen mode Exit fullscreen mode

There is no build error for a misplaced file — Next.js simply never
registers it as middleware. If moving the file to the correct location makes
it start running, this was the entire bug.

Check 2: the matcher config excludes your route

Middleware runs on every route by default unless you export a config
object with a matcher. A matcher that is too narrow — or written for a
different route shape — silently skips the requests you're testing:

// middleware.ts
export const config = {
  matcher: '/dashboard/:path*', // only /dashboard/... requests hit this
};

export function middleware(request) {
  console.log('middleware ran for', request.nextUrl.pathname);
}
Enter fullscreen mode Exit fullscreen mode

Testing / or /login against this config will never log anything —
correctly, by the matcher's own rule. Two matcher mistakes cause most
"it just doesn't run" reports:

// ❌ matches only the literal path "/dashboard", not /dashboard/settings
matcher: '/dashboard'

// ✅ the :path* segment matches the exact route and everything under it
matcher: '/dashboard/:path*'
Enter fullscreen mode Exit fullscreen mode
// ❌ forgot the API/static exclusions — middleware now runs on every
// _next/static and _next/image request too, which is usually not intended
// (this doesn't stop your route from matching, but it's the inverse bug —
// worth checking if middleware seems to run "for everything")
export const config = { matcher: '/:path*' };

// ✅ the documented catch-all pattern that excludes framework internals
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Enter fullscreen mode Exit fullscreen mode

If you need multiple, independent route groups, matcher also accepts an
array — each entry is checked independently, so one overly strict pattern does
not block the others:

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*'],
};
Enter fullscreen mode Exit fullscreen mode

Check 3: an import that fails on the Edge runtime

Middleware runs on the Edge runtime, not Node.js, even though it is a
.ts file sitting next to your Node-only code. If middleware.ts imports
something that depends on Node APIs — fs, pg, most database drivers,
crypto in its Node form — the Edge bundler can fail to build the function,
and depending on your deployment target this can surface as the middleware
being skipped rather than a build error, especially with Cloudflare's Edge
runtime constraints.

// ❌ pg speaks raw TCP — not available on the Edge runtime
import { Pool } from 'pg';

export function middleware(request) {
  const pool = new Pool(); // fails to run on the Edge
}
Enter fullscreen mode Exit fullscreen mode
// ✅ read a cookie/header directly — no database round-trip in middleware
export function middleware(request) {
  const token = request.cookies.get('session')?.value;
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}
Enter fullscreen mode Exit fullscreen mode

If middleware genuinely needs to check something a database holds (a
feature flag, a ban list), call an HTTP endpoint with fetch — the Edge
runtime supports fetch natively — instead of importing a driver that
expects a socket:

export async function middleware(request) {
  const res = await fetch(new URL('/api/session-check', request.url), {
    headers: { cookie: request.headers.get('cookie') ?? '' },
  });
  if (!res.ok) return NextResponse.redirect(new URL('/login', request.url));
}
Enter fullscreen mode Exit fullscreen mode

Verifying the fix

  1. Confirm the file path with ls middleware.ts src/middleware.ts — exactly one should exist, at the correct level.
  2. Add a console.log(request.nextUrl.pathname) as the first line and hit the exact route you expect to match; server logs (not the browser console) are where Edge middleware output appears.
  3. Temporarily remove the config.matcher export entirely — middleware then runs on every route. If it starts firing, the matcher pattern was the bug; restore a corrected pattern rather than leaving the matcher off.

Related Incidents


Originally published at https://www.iloveblogs.blog

Top comments (0)