Edge Middleware rule ordering overrides earlier patterns unexpectedly
0 reputation · 04 Jun 2023, 11:08 UTC
0 reputation · 04 Jun 2023, 11:08 UTC
When deploying an application on Vercel, two Edge Middleware files are defined with overlapping route patterns. The first rule intends to add a custom header for all paths under /api/*, while the second rule redirects /api/v1/* to a legacy service. In production, requests to /api/v1/users receive the header from the first rule instead of the redirect, indicating that the later rule is not overriding the earlier one as expected.
The goal is to understand the exact evaluation order of Edge Middleware when patterns overlap and to determine whether the declared order is the sole factor influencing precedence. Constraints include the fact that Next.js internal rewrites may also process these routes, potentially altering the outcome. Additionally, Vercel documentation states that middleware is evaluated in declaration order, but real‑world behavior suggests otherwise.
32275 reputation · 04 Jun 2023, 14:28 UTC
Declaration order is the only precedence mechanism Vercel Edge Middleware exposes, but it does not work the way your setup assumes. When two middleware files match the same request, they run top-to-bottom in declaration order, and the chain short-circuits as soon as one of them returns a response (via NextResponse.redirect(), rewrite(), or a plain Response). Your /api/* rule runs first, sets the header, and—depending on how it returns its response—either terminates the chain or passes control on. The observed behavior (header present, redirect missing) means the first rule is ending the chain before the /api/v1/* redirect ever executes.
Confirmed by the symptom: the first-declared middleware is winning. That matches documented top-to-bottom evaluation.
Likely, but verify in your code: the header rule is probably not calling NextResponse.next() in a way that lets the chain continue, or it is constructing a fresh NextResponse that swallows downstream behavior. A middleware that returns any response without explicitly continuing effectively ends processing for that request. There is no priority or order field—declaration order is the sole control.
The cleanest deterministic fix is to make the matchers non-overlapping so order stops mattering:
// middleware for the header rule — exclude the legacy path
export const config = {
matcher: ['/api/:path*'],
};
// inside the middleware:
export function middleware(req) {
if (req.nextUrl.pathname.startsWith('/api/v1/')) {
return NextResponse.next(); // let the redirect rule own these
}
const res = NextResponse.next();
res.headers.set('x-custom-header', 'value');
return res;
}
Alternatively, consolidate both behaviors into a single middleware file that checks /api/v1/* first and redirects, then falls through to the header logic. One file, one explicit order, no ambiguity.
Rewrites in next.config.js are evaluated after middleware completes. They do not change the path your middleware matchers see, so they cannot be the reason the redirect rule lost. However, they can mask debugging: a rewrite applied after middleware may make it look like a middleware redirect fired when it didn't. Check whether /api/v1/* also appears in your rewrites() config—if so, you may not need the second middleware at all.
console.error with the pathname) at the top of each middleware and inspect Vercel function logs for a request to /api/v1/users. If only one log appears, the chain is short-circuiting.NextResponse.next() (not a new Response) when it should defer./api/v1/users and expect a 3xx to the legacy service with no custom header.One caveat: short-circuit semantics of redirect()/rewrite() have shifted across Next.js versions, so confirm against your installed version's behavior with the logging test above rather than relying on documentation alone.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.