Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
Part of: React & Frontend Errors
Quick Answer
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.
The Error
Your middleware.ts file exists but never runs — requests pass through without being intercepted:
// middleware.ts — this code never executes
export function middleware(request: NextRequest) {
console.log('Middleware running:', request.url); // Never logged
return NextResponse.next();
}Or middleware runs for some routes but not others. Or after adding a matcher, the middleware stops running entirely:
export const config = {
matcher: '/dashboard/:path*', // Middleware stops running after adding this
};Or authentication redirects in middleware are ignored and protected pages render without a session check.
Why This Happens
Next.js middleware has strict requirements for where the file lives and how the matcher is configured:
- Wrong file location —
middleware.tsmust be at the project root (same level asapp/orpages/), not insideapp/,src/app/, or any subdirectory. src/directory projects — if your project uses asrc/directory,middleware.tsmust be insidesrc/, not at the root.- Invalid matcher pattern — a malformed regex or unsupported matcher syntax causes middleware to be silently skipped.
- Missing export — the middleware function must be a named export (
export function middleware) or a default export. A missing or misspelled export means Next.js cannot find it. - Middleware running on static assets — by default, middleware runs on all routes including
_next/static. Without a matcher, it intercepts requests for CSS and JS files, causing performance issues. Adding a matcher fixes this but can accidentally exclude intended routes. - Edge Runtime restrictions — middleware runs in the Edge Runtime, which does not support all Node.js APIs (
fs,crypto,child_process, etc.).
In Production: Incident Lens
How the incident surfaces. This is a security incident hiding in plain sight. When middleware silently fails to run, the app keeps working — pages render, requests succeed — but the auth-redirect logic, the rate-limit gate, the geo-block, or the AB-test bucketing that was supposed to run never fires. The team finds out one of three ways: a user reports access to a page they should not see, the security review notices unauthenticated requests reaching protected endpoints in logs, or a routine audit catches the missing redirect. The deploy that shipped the bug usually passed all functional tests because the tests never exercised the middleware-protected path from an unauthenticated state.
Blast radius. Severity depends on what the middleware was guarding. If it was authentication, every protected route is now accessible without credentials — a critical data-exposure incident. If it was a feature flag check, all users see a feature that was supposed to be staged. If it was rate limiting, the abuse window opens. The blast radius is “100% of requests that should have been blocked or rewritten” until the fix ships. Worst case is silent for days: the dashboards do not show errors because requests are succeeding, just to the wrong destination or with the wrong identity.
The monitoring signal that catches it. Standard error-rate monitoring misses this entirely. The signals that actually catch it: synthetic checks that test the middleware’s behavior end-to-end (an unauthenticated request to /dashboard should return a 307 redirect to /login; if it returns 200, middleware did not run), the custom response header trick (Fix 6 above — set X-Middleware-Ran: true and assert it on protected routes via a CI synthetic), Vercel/Cloudflare edge function logs showing middleware invocation count, and alerts on Middleware Invocations falling unexpectedly close to zero. For Vercel specifically, the Edge Network → Functions tab shows middleware execution count per deploy.
Recovery sequence. First, confirm the suspicion with a curl from outside the network: curl -i https://your-app.com/dashboard without an auth cookie — if it returns 200 instead of 307/redirect, the bug is confirmed. Second, identify what broke: middleware file path moved into app/ during a refactor, matcher regex was tightened so the path is excluded, the file moved into src/ but the project does not use src/ mode, or a next build produced a different middleware bundle than expected. Third, the immediate stabilizer is a route-level guard: add a Server Component check inside the affected layout that performs the same auth check, deploy that, then fix the middleware file in a follow-up. Never roll back the deploy without first confirming the middleware-bypass was not exploited — if it was, you need session invalidation too.
Postmortem-style preventive. The durable controls: (1) a CI synthetic test that asserts middleware fires on every protected route from an unauthenticated state — this catches the bug at PR time, not in production; (2) redundancy at the layout layer (Server Component auth check inside protected layout) so a middleware miss does not become an auth bypass; (3) explicit middleware tests using @edge-runtime/vm to exercise middleware logic in isolation; (4) X-Middleware-Ran header set unconditionally in development so missing-middleware is obvious in browser DevTools; (5) a post-deploy smoke test that fails the deployment if middleware invocation count drops to zero compared to the previous version.
Fix 1: Verify the File Location
my-nextjs-app/
├── app/ ← App Router
│ ├── layout.tsx
│ └── page.tsx
├── middleware.ts ← ✓ CORRECT — at project root
├── next.config.js
└── package.json
# With src/ directory:
my-nextjs-app/
├── src/
│ ├── app/
│ │ └── page.tsx
│ └── middleware.ts ← ✓ CORRECT — inside src/
├── next.config.js
└── package.jsonCommon wrong locations:
app/middleware.ts ← ✗ Wrong — inside app/
pages/middleware.ts ← ✗ Wrong — inside pages/
src/app/middleware.ts ← ✗ Wrong — inside src/app/
middleware/index.ts ← ✗ Wrong — in a subdirectoryCheck where Next.js is finding (or not finding) your middleware:
# Build and check for middleware detection
next build 2>&1 | grep -i middleware
# Or check the .next directory after build
ls .next/server/middleware*.js 2>/dev/null || echo "No middleware compiled"Fix 2: Fix the Matcher Configuration
The matcher config controls which paths trigger middleware. An invalid or overly restrictive matcher silently excludes routes:
Basic matcher patterns:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
console.log('Middleware for:', request.nextUrl.pathname);
return NextResponse.next();
}
export const config = {
matcher: [
// Match a specific path and all sub-paths
'/dashboard/:path*',
// Match multiple paths
'/profile/:path*',
'/settings/:path*',
// Match all paths EXCEPT static files and API routes
'/((?!_next/static|_next/image|favicon.ico|api/).*)',
// Match everything
'/:path*',
],
};The recommended production matcher (excludes static assets):
export const config = {
matcher: [
/*
* Match all request paths EXCEPT:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico
* - Public files (jpg, png, svg, etc.)
*/
'/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};Test your matcher pattern:
# Use the Next.js matcher tester (unofficial)
node -e "
const pattern = '/((?!_next/static|_next/image|favicon\\.ico).*)';
const re = new RegExp(pattern);
const paths = ['/dashboard', '/api/users', '/_next/static/chunk.js', '/favicon.ico'];
paths.forEach(p => console.log(p, '->', re.test(p)));
"Fix 3: Fix Middleware Not Running on API Routes
By default, middleware does not run on API routes unless you explicitly include them in the matcher:
// Middleware NOT running on /api/* routes? Add them to matcher:
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*', // ← Add this to intercept API routes
],
};Or use conditional logic inside middleware:
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Handle API routes differently
if (pathname.startsWith('/api/')) {
// Check API authentication
const token = request.headers.get('authorization');
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
}
// Handle page routes
if (pathname.startsWith('/dashboard')) {
const session = request.cookies.get('session');
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};Fix 4: Implement Authentication Redirects Correctly
The most common middleware use case — redirect unauthenticated users:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Routes that require authentication
const protectedRoutes = ['/dashboard', '/profile', '/settings', '/api/user'];
// Routes that should redirect to dashboard if already logged in
const authRoutes = ['/login', '/signup'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const sessionToken = request.cookies.get('session-token')?.value;
const isProtected = protectedRoutes.some(route => pathname.startsWith(route));
const isAuthRoute = authRoutes.some(route => pathname.startsWith(route));
// Redirect to login if accessing protected route without session
if (isProtected && !sessionToken) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname); // Remember where to go after login
return NextResponse.redirect(loginUrl);
}
// Redirect to dashboard if accessing login/signup while already authenticated
if (isAuthRoute && sessionToken) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/settings/:path*',
'/api/user/:path*',
'/login',
'/signup',
],
};Fix 5: Fix Edge Runtime Restrictions
Middleware runs in the Edge Runtime — a restricted environment without full Node.js APIs:
What is NOT available in middleware:
// ✗ These will throw in middleware
import fs from 'fs'; // No filesystem access
import { createHash } from 'crypto'; // No Node.js crypto
import { execSync } from 'child_process'; // No child processes
import prisma from '@/lib/prisma'; // No database connections (TCP)What IS available:
// ✓ These work in middleware
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // JWT verification (Edge-compatible)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (token) {
try {
// jose is Edge-compatible (unlike jsonwebtoken)
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET)
);
// Add user info to request headers for downstream use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.sub as string);
return NextResponse.next({ request: { headers: requestHeaders } });
} catch {
// Invalid token — redirect to login
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.redirect(new URL('/login', request.url));
}Use next/headers in Server Components for database-dependent auth:
// app/dashboard/layout.tsx — Server Component (not middleware)
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { validateSession } from '@/lib/auth'; // Can use Prisma here
export default async function DashboardLayout({ children }) {
const cookieStore = cookies();
const session = cookieStore.get('session-token');
if (!session || !(await validateSession(session.value))) {
redirect('/login');
}
return <>{children}</>;
}Fix 6: Add Debugging to Middleware
Since middleware runs on the server, console.log output appears in the terminal running next dev, not in the browser:
export function middleware(request: NextRequest) {
// Logs appear in your terminal, not the browser console
console.log('[Middleware]', {
url: request.url,
pathname: request.nextUrl.pathname,
method: request.method,
cookies: Object.fromEntries(request.cookies),
});
return NextResponse.next();
}Add a custom response header for debugging (visible in browser DevTools):
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Visible in Network tab → Response Headers
response.headers.set('X-Middleware-Ran', 'true');
response.headers.set('X-Middleware-Path', request.nextUrl.pathname);
return response;
}Check the Network tab in DevTools — if X-Middleware-Ran: true is present in the response, middleware is running. If it’s missing, middleware is not matching the route.
Still Not Working?
Check for a TypeScript compilation error in middleware. If middleware.ts has a type error, Next.js may silently skip it. Run:
npx tsc --noEmitVerify the middleware is compiled in .next:
next build
ls .next/server/middleware.js
# If this file is missing, middleware.ts is not being picked upCheck the Next.js version. Middleware support and the matcher configuration changed significantly across versions:
- Next.js 12 — introduced middleware
- Next.js 13 — renamed
_middleware.tstomiddleware.ts - Next.js 13.1+ — added
matcherconfig with regex support
cat package.json | grep '"next"'
# Ensure you're on 13.1+ for full matcher supportCheck for conflicting next.config.js rewrites. URL rewrites in next.config.js run before middleware. If a rewrite changes the URL path before middleware sees it, the matcher may not match:
// next.config.js — rewrites run BEFORE middleware
module.exports = {
async rewrites() {
return [
{ source: '/old-dashboard', destination: '/dashboard' },
// Middleware sees '/old-dashboard', not '/dashboard' — matcher for '/dashboard' won't fire
];
},
};For related Next.js issues, see Fix: Next.js Build Failed, Fix: Next.js Environment Variables Not Working, Fix: Next.js API Route Not Working, and Fix: Next.js Hydration Failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React Hydration Error — Text Content Does Not Match
How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.
Fix: Next.js App Router Fetch Not Caching or Always Stale
How to fix Next.js App Router fetch caching issues — understanding cache behavior, revalidation with next.revalidate, opting out with no-store, cache tags, and debugging stale data.
Fix: Next.js Build Failed (next build Errors and How to Fix Them)
How to fix Next.js build failures — TypeScript errors blocking production builds, module resolution failures, missing environment variables, static generation errors, and common next build crash causes.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.