Skip to content

Error: Cookies can only be modified in a Server Action or Route Handler. #77234

@alireza-asgharii

Description

@alireza-asgharii

Issue: Logging Out Users on 401 Error in Next.js with auth.js

Scenario:

While fetching data on the server side, I use a custom wrapper function based on fetch as a centralized API handler for my application. When the server returns a 401 (Unauthorized) error, I expected to be able to log the user out (or take similar actions), just like how Axios interceptors handle such cases.

Initial Implementation:

To handle this, I implemented the following logic in my fetch function:

  • I use auth.js (next-auth) to manage user sessions.
  • If a 401 error occurs, I call signOut to log the user out.

Problem:

When executing signOut, the following error occurs:

Error: Cookies can only be modified in a Server Action or Route Handler.

This makes sense, as fetch runs on the server, and Next.js does not allow direct modification of cookies at this level.
(This issue has also been discussed in a previous GitHub Issue.)

Even manually attempting to delete cookies did not work.
I tried the following approaches, but none resolved the issue:

  1. Calling signOut directly on the server
  2. Executing signOut inside a Server Action
  3. Calling signOut from within a Route Handler

Proposed Solution:

To resolve this issue, I created a Route Handler and implemented the logout operation within it.
Instead of calling signOut directly inside the fetch function, I created an API Route that handles the logout process.

🚀 The trick:

  • In the fetch function, when a 401 error is detected, I redirect the user to this API Route.
  • The GET method of this route immediately logs the user out.
  • This approach is similar to how many authentication services handle session invalidation.

Implementation:

1. Centralized Fetch Function

/* eslint-disable @typescript-eslint/no-explicit-any */
"server-only";

import { redirectTo } from "@/actions";
import { auth } from "@/auth";

type FetchApiResponse<T, ET> = {
  status: number;
  isError: boolean;
  errorData: ET | null;
  data: T | null;
  message: string | null;
};

// Fetch API Proxy (Middleware)
async function fetchApiProxy<T, ET = any>(
  url: string,
  options?: RequestInit & { authorize?: boolean },
) {
	  const res = await fetchApi<T, ET>(url, options);

	  if (res.status === 401 || res.message === "UnAuthentication") {
	    await redirectTo("/api/logout");
	  }

	  return res;
	}

// Main Fetch API
async function fetchApi<T, ET = any>(
  url: string,
  options?: RequestInit & { authorize?: boolean },
): Promise<FetchApiResponse<T, ET>> {
  try {
const session = await auth();

const res = await fetch(`${process.env.API_BASE_URL}${url}`, {
  ...options,
  headers: {
    "Content-Type": "application/json",
    ...options?.headers,
    ...(options?.authorize && {
      Authorization: `Bearer ${session?.accessToken}`,
    }),
  },
});

const data = await res.json();

if (!res.ok) {
  return {
    isError: true,
    message: data?.message || res.statusText || "Unknown error",
    data: null,
    status: res.status,
    errorData: data?.data ?? null,
  };
}

return {
  isError: false,
  message: data?.message || null,
  data: data?.data,
  errorData: null,
  status: res.status,
};
   } catch (error) {
console.log({ fetchApiError: error });
return {
  isError: true,
  message: error instanceof Error ? error.message : "Unknown error",
  data: null,
  status: 500,
  errorData: null,
};
  }
}

export { fetchApiProxy as fetchApi };
export { fetchApi as fetchApiRaw };

2. Logout Route Handler

import { signOut } from "@/auth";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function GET() {
  const cookieStore = cookies();
  const cookieKeys = ["authjs.session-token", "next-auth.session-token", "access_token"];

  cookieKeys.forEach((key) => cookieStore.delete(key));

  await signOut({ redirect: false });

  return NextResponse.redirect("/login");
}

Outcome:

🔹 401 errors are now handled in a clean and structured manner.
🔹 The issue with modifying cookies on the server is resolved.
🔹 The fetch function remains clean, without relying on client-side methods that cannot execute on the server.

⚡ This approach not only fixes the issue but also provides a standardized pattern for handling authentication in Next.js + auth.js.


Suggestion for Next.js:
Adding support for handling signOut on the server without requiring an API Route or providing an official solution for this challenge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    invalid linkThe issue was auto-closed due to a missing/invalid reproduction link. A new issue should be opened.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions