Next.js Middleware Limits and Redirect Loops with trailingSlash
- Author
- Vishal Maurya
- Published on
- Reading time
- 4 min read
Overview
Two problems show up often. First, you try to use a Node library or heavy logic in middleware.ts and the build complains or behavior differs. Second, you add a redirect and the browser reports too many redirects, frequently because trailingSlash is also redirecting. Both come from misunderstanding what middleware is.
Naming and capabilities have changed across versions. Recent releases have moved toward a Node.js runtime option and a rename of the file convention, so verify details in the docs for your version.
1. What Middleware Is For
Middleware runs before a request is completed. It can rewrite, redirect, set headers, set cookies, and respond directly. It is well suited to lightweight, request-level decisions: redirecting unauthenticated users to a login route, locale detection, simple A/B routing, and adding headers.
It is not a good place for database queries, heavy computation, or authorization that must be authoritative. Treat it as an optimistic first check and verify permissions again close to the data.
2. Runtime Constraints
By default middleware runs in the Edge runtime. That means:
- Many Node.js APIs and packages that depend on them (such as
fsor some database drivers) are unavailable. - There are code size limits for the bundle, depending on platform.
- Long-running work is unsuitable.
Newer Next.js versions allow selecting the Node.js runtime for middleware. If you need Node APIs, check whether your version supports that option and whether your hosting platform does. Otherwise, move the logic into a Server Component, Route Handler, or Server Action.
Middleware also cannot read or modify the response body of pages in the way a server framework middleware can; it works on requests and can set response headers or return its own response.
3. Scope It With a Matcher
By default middleware runs for many requests, including static assets. Narrow it with matcher:
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const hasSession = request.cookies.has('session');
if (!hasSession && request.nextUrl.pathname.startsWith('/account')) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('next', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ['/account/:path*'],
};
Note the cookie check only proves a cookie exists, not that it is valid. Validate the session in your data layer.
4. How trailingSlash Changes Things
With trailingSlash: true in next.config.js, Next.js redirects /about to /about/. With the default false, it redirects /about/ to /about. These redirects are generated by the framework, and their interaction with middleware depends on your version, so test the actual sequence of requests.
The classic loop: middleware or an external rule normalizes URLs to remove the trailing slash, while trailingSlash: true adds it back. Each response redirects to the other form until the browser gives up.
5. Diagnose a Loop
Use curl to see the chain:
curl -sIL -o /dev/null -w '%{http_code} %{url_effective}\n' --max-redirs 5 https://example.com/about
Or inspect each hop manually without following:
curl -sI https://example.com/about | grep -i -E '^(HTTP|location)'
curl -sI https://example.com/about/ | grep -i -E '^(HTTP|location)'
If the two forms point at each other, you have two components disagreeing. Common sources: middleware, redirects() in next.config.js, a CDN or proxy rule, or a hosting platform setting.
6. Fix It With One Source of Truth
Choose one URL form and enforce it in one place. If you use trailingSlash: true, make sure your middleware paths and redirect targets include the slash, or compare against a normalized path:
// middleware.ts (normalize before comparing)
export function middleware(request: NextRequest) {
const path = request.nextUrl.pathname.replace(/\/+$/, '') || '/';
if (path === '/old-page') {
const url = request.nextUrl.clone();
url.pathname = '/new-page/'; // matches trailingSlash: true
return NextResponse.redirect(url, 308);
}
return NextResponse.next();
}
Also use a redirect status that matches intent: permanent redirects for stable moves, temporary for conditional logic like login.
7. Exclude Internal Paths
Make sure the matcher does not capture _next/static, image optimization routes, or files with extensions unless you mean to. Accidentally redirecting those can look like a broken site or cause loops. Prefer an explicit allowlist of paths over a catch-all.
Conclusion
Keep middleware small, scoped, and stateless. For URL normalization, pick one owner and make every redirect target consistent with your trailingSlash setting. Test with curl against a production build rather than the dev server.
If you hit redirect loops, SEO canonicalization problems, or auth gating issues in a Next.js app, I can help audit the request flow. Contact me.