How to Fix: cookie can’t modified in “use server” file
The bug is not in your cookie API call itself—it is in where that call runs. In Next.js 15, cookies can only be mutated inside places that own the outgoing HTTP response, such as a Server Action or Route Handler. A shared use server file like protect.ts may run on the server, but that does not automatically mean it can modify response cookies.
Table of Contents
Understanding the Root Cause
Next.js separates reading cookies from writing cookies.
- Reading cookies: allowed in more server-side contexts, including Server Components.
- Writing cookies: only allowed when Next.js can attach a Set-Cookie header to the HTTP response.
That is why code like this often fails when placed in a utility file such as protect.ts:
'use server'
import { cookies } from 'next/headers'
export async function protect() {
const cookieStore = await cookies()
cookieStore.set('token', 'abc')
}
Even though the file uses 'use server', the function is still just a server-side module function unless it is executed in a valid mutation context. Next.js blocks the write because there is no guaranteed response object available for that call site.
In practice, this issue usually appears in one of these cases:
- You call
cookies().set()inside a shared helper imported by a Server Component. - You try to update cookies during rendering.
- You put auth logic in a server-only file and expect it to behave like a Route Handler.
The fix is to move cookie mutation into one of the supported boundaries:
- Route Handlers such as
app/api/.../route.ts - Server Actions triggered from forms or client interactions
- In some auth flows, middleware can also read and set cookies, depending on the use case
Step-by-Step Solution
The safest pattern is: keep protect.ts as a pure auth/helper module, and move actual cookie writes into a Route Handler or Server Action.
1. Do not mutate cookies directly inside a shared helper
Refactor protect.ts so it only computes values or validates state.
import { cookies } from 'next/headers'
export async function getAuthCookie() {
const cookieStore = await cookies()
return cookieStore.get('token')?.value ?? null
}
export async function shouldRefreshToken() {
const cookieStore = await cookies()
const token = cookieStore.get('token')?.value
if (!token) return false
// your validation logic here
return true
}
2. Move cookie writes into a Route Handler
If you need to set, refresh, or delete a cookie during authentication, create an API route.
import { NextResponse } from 'next/server'
export async function POST() {
const response = NextResponse.json({ success: true })
response.cookies.set('token', 'new-token-value', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
return response
}
This works because NextResponse owns the outgoing response and can attach the Set-Cookie header correctly.
3. Or move cookie writes into a Server Action
If your cookie update is triggered by a form submit or a UI action, use a Server Action.
'use server'
import { cookies } from 'next/headers'
export async function loginAction() {
const cookieStore = await cookies()
cookieStore.set('token', 'new-token-value', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
})
}
Then call it from a form or client interaction:
import { loginAction } from './actions'
export default function LoginForm() {
return (
<form action={loginAction}>
<button type="submit">Login</button>
</form>
)
}
This works because a Server Action runs in a mutation-capable request/response cycle.
4. If you are protecting routes, separate validation from mutation
A common mistake is trying to refresh tokens while checking access inside a protection helper. Instead:
- Use protect.ts to verify whether a user is authenticated.
- Use a Route Handler to refresh or rotate tokens.
- Redirect unauthenticated users from the page or layout layer.
import { redirect } from 'next/navigation'
import { getAuthCookie } from '@/lib/protect'
export default async function DashboardPage() {
const token = await getAuthCookie()
if (!token) {
redirect('/login')
}
return <div>Dashboard</div>
}
5. If you need middleware behavior, use middleware explicitly
For request-time auth checks, use middleware.ts instead of trying to make a helper behave like middleware.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
If you need to set a cookie in middleware, do it on the returned NextResponse, not in a random helper.
6. Verify your Next.js cookie syntax
Depending on the version, cookies() may be asynchronous in current examples and docs. Use the modern pattern consistently:
import { cookies } from 'next/headers'
export async function example() {
const cookieStore = await cookies()
return cookieStore.get('token')
}
For the latest behavior and API details, check the Next.js cookies documentation.
Common Edge Cases
Calling a Server Action like a normal function
If you import a supposed action and invoke it during rendering, Next.js may not treat it as a valid cookie mutation boundary. A Server Action should be triggered through supported action flows.
Trying to set cookies in a Server Component render path
Server Components can read request data, but rendering is not the same thing as building a mutable response. If your page, layout, or helper runs during render, cookie writes can fail.
Refreshing auth tokens inside a utility imported everywhere
This is a design trap. Shared utilities should usually return auth state, not perform response mutation. Put token refresh in a dedicated endpoint or action.
Forgetting cookie options
A cookie may appear not to update if attributes differ from what the browser expects. Double-check path, httpOnly, secure, sameSite, and expiration settings.
Development vs production behavior
If you set secure: true locally over plain HTTP, the browser may ignore the cookie. Use:
secure: process.env.NODE_ENV === 'production'
Deleting a cookie incorrectly
Deletion must match the correct path and attributes in many cases. If the original cookie was set on /, delete it with the same path context.
response.cookies.set('token', '', {
expires: new Date(0),
path: '/',
})
FAQ
Why does 'use server' not guarantee that I can modify cookies?
Because 'use server' only marks code as server-executed. Cookie mutation additionally requires a context that can send a Set-Cookie header in the HTTP response, such as a Route Handler or Server Action.
Can I read cookies in protect.ts but not write them?
Yes. Reading is allowed in more places. Writing is restricted to response-aware contexts.
What is the best fix for auth protection in this issue?
Use protect.ts for auth checks only, then move cookie updates like login, refresh, logout, or token rotation into a Route Handler or Server Action. That matches how Next.js 15 expects response mutations to work.
In short, the issue happens because server code is not always the same as response-mutation code. Once cookie writes are moved out of protect.ts and into a proper Next.js response boundary, the bug disappears.
If you want to compare this behavior against the original reproduction, review the project linked in the issue: reproduction repository.