Next.js Discord

Discord Forum

Error: Cookies can only be modified in a Server Action or Route Handler - Next.js 15 with cookies()

Unanswered
Siberian posted this in #help-forum
Open in Discord
SiberianOP
Hello everyone,

I'm working with Next.js 15 and trying to set a session cookie after a user clicks a forgot password link. However, I'm encountering the following error when attempting to set the cookie:
Error: Cookies can only be modified in a Server Action or Route Handler. 

In my code, I'm calling a setSessionCookie function to create a session token and set the cookie for the session. The error occurs when I try to use the cookies() API to set the cookie. From the error message, it seems that I am trying to modify cookies outside of a valid server context (Server Action or Route Handler).

Here is the relavent code:
// session.ts
export async function setSessionCookie(userId: string, ipAddress: string): Promise<void> {
  const sessionToken = await generateSessionToken();
  const session = await createSession(sessionToken, userId, ipAddress);
  const cookieStore = await cookies();

  cookieStore.set("session", sessionToken, {
    httpOnly: true,
    path: "/",
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: (session.expiresAt - Date.now()) / 1000,
  });
}

// forgotPasswordCodeAction.ts
"use server";

import prisma from "@/lib/db/prisma/prisma";
import { setSessionCookie } from "@/lib/auth/session";
import { redirect } from "next/navigation";
import { getIpAddress } from "../../utils";

export default async function forgotPasswordCodeAction(code: string) {
  const user = await prisma.user.findFirst({
    where: { forgotPasswordLink: code },
  });

  if (!user?.id) {
    redirect("/");
  }

  await setSessionCookie(user.id, await getIpAddress());

  redirect("/settings/account");
}

As well as where the action is called:

13 Replies

SiberianOP
import forgotPasswordCodeAction from "./actions";

export default async function ForgotPasswordCodePage({
  params,
}: {
  params: Promise<{ code: string }>;
}) {
  const { code } = await params;

  console.log(code);

  await forgotPasswordCodeAction(code);

  return <h1>Processing request</h1>;
}

Thank you.
SiberianOP
This is what I get in the terminal by the way:
 24 |   const cookieStore = await cookies();
  25 |
> 26 |   cookieStore.set("session", sessionToken, {
     |              ^
  27 |     httpOnly: true,
  28 |     path: "/",
  29 |     secure: process.env.NODE_ENV === "production", {
I've had this issue before, and it seemed like the example at the top of the [Next.js cookies documentation](https://nextjs.org/docs/app/api-reference/functions/cookies) was misleading (for me) as it only includes reading the cookies, and I automatically assumed you could write them too.

You can only read cookies from server components, and write them from server actions. So, I found I could set them by calling the action within useEffect in a client component, rather than trying to write them from a server component.

"use client"
import { useEffect } from "rect";
import { getCookie } from "@/app/_actions/cookies";

export function Component() {

    const [cookie, setCookie] = useState(null);

    useEffect(() => {
        async function fetchCookies() {
            const cookieData = await getCookie(); // Server action
            setCookie(cookieData);
        }

    }, [cookie]);

    
    if (cookie) {
        // ... logic
    }

}


Which in the end (when I think about it) makes sense as a server component wouldn't be able to write client data. (?)
No since the server component can only read cookies not write them
@clarity No since the server component can only read cookies not write them
SiberianOP
Alright, I'll use the client compoenent approach and call the action like:
"use client";

import { useEffect, useState } from "react";
import forgotPasswordCodeAction from "./actions";
import { useRouter } from "next/navigation";

export default function ForgotPasswordCodePage({
  params,
}: {
  params: { code: string };
}) {
  const router = useRouter();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function handleForgotPassword() {
      try {
        await forgotPasswordCodeAction(params.code);
        router.push("/settings/account");
      } catch (err) {
        console.error("Error processing forgot password:", err);
        setError("Failed to process request.");
      } finally {
        setLoading(false);
      }
    }

    handleForgotPassword();
  }, [params.code, router]);

  if (loading) return <h1>Processing request...</h1>;
  if (error) return <h1>{error}</h1>;

  return <h1>Redirecting...</h1>;
}

Thanks.
Asian black bear
The reason why you got the issue in the first place is that cookies (which are basically headers) cannot be written after a server has finished sending headers and is streaming the body/content of a document.
Consequently a page is just the result of a GET request and should not mutate on its own.
Both of these imply that you cannot write cookies within a page/server component and instead it can only happen as part of a route handler or server action that is invoked from the client.
invoked from the client
thats the most important part
calling a server action from a page on load, will not work.
neither will a route handler