How to Fix: Use Set-Cookie header instead of x-middleware-set-cookie
The bug is caused by a mismatch between how Next.js middleware communicates cookie mutations and how libraries like iron-session expect cookies to be written. If your session appears to save inside middleware but disappears on the next request, the real problem is usually that the response carries x-middleware-set-cookie instead of a standard Set-Cookie header.
In practical terms, this means middleware can stage cookie changes for internal Next.js processing, but downstream tooling, browsers, and some session workflows may not treat that header the same way as a normal cookie response. The fix is to ensure the final HTTP response exposes a real Set-Cookie header or to move session writes out of middleware into a route handler or server action where standard cookie semantics are guaranteed.
Understanding the Root Cause
Next.js middleware runs in a special execution layer. When you call cookie mutation APIs there, Next.js may serialize those updates into x-middleware-set-cookie rather than directly emitting Set-Cookie on the network response at that stage.
That behavior becomes a problem when using session libraries such as iron-session because they are designed around normal HTTP cookie behavior:
- The server writes a Set-Cookie header.
- The browser stores it.
- The next request sends it back through the Cookie header.
When middleware uses x-middleware-set-cookie, the cookie may not be available in the way your session library expects, especially if:
- You try to read the session immediately in the next handler.
- You depend on browser-visible cookie persistence.
- You mix middleware cookie writes with API routes, route handlers, or server components.
So the issue is not usually that iron-session is broken. The issue is that middleware is not the right place for persistent session writes when the library expects standard response headers.
The safest rule is simple: read lightweight cookies in middleware, but write session cookies in route handlers or API routes using real Set-Cookie headers.
Step-by-Step Solution
The most reliable fix is to stop saving the session in middleware and instead perform the write in a Route Handler or API route.
1. Do not persist the session inside middleware
If your middleware looks conceptually like this, that is the source of the bug:
import { NextResponse } from 'next/server'
export async function middleware(request) {
const response = NextResponse.next()
// Problematic pattern: mutating session/cookies here
response.cookies.set('session', 'value', {
httpOnly: true,
path: '/',
secure: process.env.NODE_ENV === 'production',
})
return response
}
This can produce internal middleware cookie headers instead of the final Set-Cookie header your session flow needs.
2. Move session creation into an API route or Route Handler
For the App Router, use a route handler such as app/api/session/route.ts:
import { NextResponse } from 'next/server'
export async function POST() {
const response = NextResponse.json({ ok: true })
response.cookies.set('session', 'value', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
return response
}
This ensures the final response contains a standard Set-Cookie header.
3. If using iron-session, save it in the route handler
Use iron-session where the framework can produce a standard HTTP response. Example pattern:
import { NextResponse } from 'next/server'
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
const sessionOptions = {
password: process.env.SESSION_PASSWORD,
cookieName: 'myapp-session',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
}
export async function POST() {
const cookieStore = await cookies()
const session = await getIronSession(cookieStore, sessionOptions)
session.user = { id: '123' }
await session.save()
return NextResponse.json({ ok: true })
}
The exact iron-session API can vary by Next.js version, but the architectural fix stays the same: save the session in a handler that returns a normal response, not in middleware.
4. Keep middleware focused on redirects, rewrites, and guards
Middleware is still useful for reading cookies and making routing decisions:
import { NextResponse } from 'next/server'
export function middleware(request) {
const session = request.cookies.get('myapp-session')
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
This is a good middleware use case because it reads state instead of trying to persist a complex session there.
5. Verify the fix in browser devtools
After moving the session write logic:
- Open the network tab.
- Trigger the login or session-creation request.
- Inspect the response headers.
- Confirm that Set-Cookie is present.
- Confirm the next request includes the expected Cookie header.
If you only see x-middleware-set-cookie, the write is still happening in middleware or in a flow that gets transformed by middleware internals.
6. If you must bridge middleware logic, redirect to a handler that sets the cookie
Sometimes you want middleware to detect a condition and initiate a session update. In that case, let middleware redirect to a dedicated route that performs the actual write:
import { NextResponse } from 'next/server'
export function middleware(request) {
const shouldRefresh = request.nextUrl.searchParams.get('refresh') === '1'
if (shouldRefresh) {
return NextResponse.redirect(new URL('/api/refresh-session', request.url))
}
return NextResponse.next()
}
Then set the cookie in the route handler:
import { NextResponse } from 'next/server'
export async function GET() {
const response = NextResponse.redirect(new URL('/dashboard', process.env.NEXT_PUBLIC_APP_URL))
response.cookies.set('session', 'new-value', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
return response
}
This preserves your middleware-driven control flow while keeping cookie persistence on a proper response.
Common Edge Cases
1. Cookie appears in development but fails in production
This is often caused by secure, domain, or sameSite settings. For example:
- secure: true requires HTTPS.
- An incorrect domain prevents the browser from storing the cookie.
- sameSite=’strict’ can block some auth return flows.
Even after fixing the middleware issue, these attributes must still be correct.
2. Session is set, but middleware still cannot read it immediately
If the cookie is written on one response, middleware will only see it on the next incoming request. Middleware cannot retroactively read a cookie that is being created later in the same response chain.
3. Rewrites and redirects hide the real response behavior
If middleware rewrites to another destination, you may inspect the wrong request in devtools. Always check the final response that is responsible for setting the cookie.
4. Multiple cookie writers override each other
If middleware, route handlers, and auth libraries all touch the same cookie name, later writes can replace earlier ones. Use one clear owner for the session cookie.
5. Edge runtime compatibility issues
Some session libraries depend on Node.js behavior that is not fully compatible with the Edge Runtime. Even if cookie headers are corrected, verify that your session library officially supports your runtime target.
6. CDN or proxy layers strip headers
If you deploy behind a reverse proxy, make sure Set-Cookie headers are forwarded unchanged. This is less common than the middleware issue, but it can look identical from the browser side.
FAQ
Can I ever set cookies in Next.js middleware?
Yes, but it is best for simple scenarios where Next.js middleware cookie handling is sufficient. For persistent authenticated sessions with libraries like iron-session, writing cookies in a route handler or API route is much more reliable.
Why does Next.js use x-middleware-set-cookie at all?
Because middleware runs in a specialized pipeline where Next.js may carry response mutations internally before finalizing the outgoing response. That internal transport is not always equivalent to a standard browser-facing Set-Cookie flow for every integration.
What is the best long-term fix for this issue?
The best fix is architectural: use middleware to read and gate requests, and use route handlers, API routes, or server-side auth endpoints to write session cookies. That aligns your app with standard HTTP behavior and avoids fragile middleware-specific cookie semantics.
If you are reproducing this issue with the sample repository linked in the GitHub report, the practical resolution is to refactor session persistence out of middleware, verify that the final response includes Set-Cookie, and keep middleware limited to redirects, rewrites, and lightweight cookie reads.