Next.js 16: Migrate middleware.ts to proxy.ts

Photo by Taylor Vick on Unsplash
In Next.js 16, released October 21, 2025, the middleware.ts convention was renamed to proxy.ts, and the exported function changed from middleware to proxy. The logic is identical, but proxy runs only on the Node.js runtime. A leftover middleware.ts is deprecated and ignored at build time.
Run the official upgrade codemod, npx @next/codemod@canary upgrade latest, which migrates the convention for you. To do it by hand, rename middleware.ts to proxy.ts, rename the exported function to proxy, and rename config flags such as skipMiddlewareUrlNormalize to skipProxyUrlNormalize.
No. proxy runs only on the Node.js runtime and the runtime cannot be configured. If you genuinely need the Edge runtime, you keep the deprecated middleware.ts file for now, until Next.js ships further Edge runtime guidance in a later release.
No. The Next.js docs state proxy should not be used as a full session management or authorization solution, only for optimistic checks. CVE-2025-29927 showed why: a spoofed x-middleware-subrequest header could bypass middleware entirely. Real authorization belongs in your Server Components, Route Handlers, and Server Actions.
CVE-2025-29927 is a CVSS 9.1 Next.js flaw, published March 21, 2025, that let attackers bypass middleware by spoofing the internal x-middleware-subrequest header. It was patched in 12.3.5, 13.5.9, 14.2.25, and 15.2.3. The lesson reinforced by the proxy rename is that the request-interception layer is not a security boundary.

Photo by Taylor Vick on Unsplash
Key Takeaway
Next.js 16, released October 21, 2025, renames the middleware.ts file to proxy.ts and its exported function to proxy. The logic is identical, but it now runs only on the Node.js runtime. Because a leftover middleware.ts is silently ignored, and because proxy is not an authentication boundary, authorization must live in your route and data layer.
If you upgrade a project to Next.js 16 and your login redirect quietly stops firing, the cause is almost always the same: your middleware.ts file no longer runs. Next.js 16 renamed the whole convention. I hit this on my own portfolio, and the fix is mechanical — but the reasons behind it are worth understanding before you rename a single file.
This post walks through exactly what changed, the one-line codemod that does the boring part, the trap that leaves protected routes wide open, and the single most important lesson the Next.js team keeps repeating: the request-interception layer was never meant to be your security boundary.
Next.js 16 shipped on October 21, 2025. Alongside stable Turbopack and Cache Components, it renamed middleware.ts to proxy.ts. The file does the same job — it runs code before a request is completed, so you can rewrite, redirect, or modify headers — but the name now describes what it really is: a proxy that sits at your app's network boundary.
Two things move with the rename. The file becomes proxy.ts, and the exported function becomes proxy. Config flags that carried the old name change too — skipMiddlewareUrlNormalize becomes skipProxyUrlNormalize. One detail matters a lot: proxy runs only on the Node.js runtime. The edge runtime is not supported in proxy, and the runtime cannot be configured. If you genuinely need the edge runtime, you keep the deprecated middleware file for now.
// middleware.ts (Next.js 15 and earlier)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}
// ---------------------------------------------
// proxy.ts (Next.js 16+) — runs on the Node.js runtime
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// same logic; only the file name and function name change
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}The old name caused years of confusion with Express-style middleware, making the feature sound more general-purpose than it is. Reading proxy.ts, a new teammate immediately knows this file intercepts requests at the network edge rather than wrapping every handler in your app.
The rename is partly cosmetic, but it lands against a real security backdrop. CVE-2025-29927, published March 21, 2025 with a CVSS score of 9.1, let an attacker bypass Next.js middleware entirely by sending a spoofed x-middleware-subrequest header. That header exists internally to stop middleware from calling itself in a loop; when it was trusted blindly, any request carrying it skipped middleware execution.
The advisory's wording is blunt: it is possible to bypass authorization checks within a Next.js application if the authorization check occurs in middleware. It was patched in 12.3.5, 13.5.9, 14.2.25, and 15.2.3 — but the deeper lesson outlived the patch. A file that intercepts requests at the edge is the wrong place to be the only thing standing between an attacker and your data.
The change itself is trivial. The official version-16 upgrade codemod migrates the middleware convention to proxy for you, along with the renamed config flags. If you prefer to do it by hand, rename the file, rename the function, and update any flags. Here is the whole procedure.
# 1. Automated: the v16 upgrade codemod migrates the convention for you
npx @next/codemod@canary upgrade latest
# 2. Or do it by hand — rename the file
mv middleware.ts proxy.ts
# (then rename the exported function: middleware -> proxy)
# 3. Rename any config flags that carried the old name
# skipMiddlewareUrlNormalize -> skipProxyUrlNormalizeA leftover middleware.ts is ignored at build time in Next.js 16 with no error. Your auth redirects and rewrites simply stop executing, and routes you thought were protected become publicly reachable — silently. After upgrading, search your repo for any stray middleware file and confirm it was renamed, not left behind.
This is the part to internalize. The Next.js docs state plainly that proxy should not be used as a full session management or authorization solution. It is fine for optimistic checks — a fast, best-effort redirect that improves the experience — but it is not a security control. The real authorization check belongs where the data lives.
In practice, that means verifying the session in your Server Components, Route Handlers, and Server Actions — as close to the database as you can get. Do the check right before you read or mutate data, not one hop earlier at the request boundary. If proxy is bypassed, spoofed, or accidentally renamed away, a data-layer check still holds the line. Proxy becomes a UX optimization on top of real enforcement, never the enforcement itself.
There is still an edge you should harden. For the CVE specifically, the mitigation for anyone who cannot patch immediately is to prevent external requests containing the x-middleware-subrequest header from reaching your Next.js application. You strip or block that header at your load balancer, reverse proxy, or CDN before it ever hits the app.
Managed platforms like Vercel apply this protection automatically, but on a self-hosted deployment it is your responsibility. I run behind a reverse proxy, so I drop the x-middleware-subrequest header there as defense in depth — even on a patched version, no legitimate external client should ever send an internal Next.js header.
| Aspect | middleware.ts (15 and earlier) | proxy.ts (16 and newer) |
|---|---|---|
| File status | Deprecated in Next.js 16, removed later | Current convention |
| Runtime | Edge or Node.js | Node.js only, not configurable |
| Exported function | middleware | proxy |
| Config flag | skipMiddlewareUrlNormalize | skipProxyUrlNormalize |
| Security role | Never a real auth boundary (CVE-2025-29927) | Still not an auth boundary — optimistic checks only |
The rename is a small diff, but treat it as a prompt. Move any real authorization out of proxy and into your route and data layer, strip internal headers at your edge, and let proxy do the one thing its new name promises — intercepting requests at the network boundary, nothing more.