admin管理员组

文章数量:1313149

I'm currently developing a Next.js 14 application and have encountered an issue with modifying cookies. Despite following the Next.js documentation on server actions and cookie manipulation, I'm receiving an error when trying to set cookies during the signout process. The error message states: "Cookies can only be modified in a Server Action or Route Handler.

Here's the relevant code snippet from my project:

// pwa/src/utils/globalActions.ts

'use server';

import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

export const signout = async () => {
  cookies.set('token', '', { maxAge: -1 });
  cookies.set('refreshToken', '', { maxAge: -1 });
  cookies.set('user', '', { maxAge: -1 });
  return redirect('/auth/signin');
};
// pwa/src/utils/apiAxios.ts (This file is used by both client and server ponents)

import axios, {
    type AxiosResponse,
    type AxiosError,
    type AxiosInstance,
} from 'axios';
import { signout } from './globalActions';

const Axios = axios.create({
    baseURL: ENTRYPOINT,
    headers: {
        Accept: 'application/ld+json',
        'Content-Type': 'application/ld+json',
    },
});

Axios.interceptors.response.use(
    response => response,
    async (error: AxiosError<ApiResponseError>) => {
        if (error.status === 401 || error.response?.status === 401) {
            await signout();
        }

        return Promise.reject(error);
    },
);

I've checked my usage against the Next.js documentation and made sure to use the server execution context ('use server';). Yet, the issue persists. I suspect I might be missing a detail in how Next.js 14 handles cookies in server actions or there might be a specific configuration step I've overlooked.

Has anyone encountered this issue before, or does anyone have insights on how to properly modify cookies within server ponents in Next.js 14? Any help or pointers would be greatly appreciated.

I'm currently developing a Next.js 14 application and have encountered an issue with modifying cookies. Despite following the Next.js documentation on server actions and cookie manipulation, I'm receiving an error when trying to set cookies during the signout process. The error message states: "Cookies can only be modified in a Server Action or Route Handler.

Here's the relevant code snippet from my project:

// pwa/src/utils/globalActions.ts

'use server';

import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

export const signout = async () => {
  cookies.set('token', '', { maxAge: -1 });
  cookies.set('refreshToken', '', { maxAge: -1 });
  cookies.set('user', '', { maxAge: -1 });
  return redirect('/auth/signin');
};
// pwa/src/utils/apiAxios.ts (This file is used by both client and server ponents)

import axios, {
    type AxiosResponse,
    type AxiosError,
    type AxiosInstance,
} from 'axios';
import { signout } from './globalActions';

const Axios = axios.create({
    baseURL: ENTRYPOINT,
    headers: {
        Accept: 'application/ld+json',
        'Content-Type': 'application/ld+json',
    },
});

Axios.interceptors.response.use(
    response => response,
    async (error: AxiosError<ApiResponseError>) => {
        if (error.status === 401 || error.response?.status === 401) {
            await signout();
        }

        return Promise.reject(error);
    },
);

I've checked my usage against the Next.js documentation and made sure to use the server execution context ('use server';). Yet, the issue persists. I suspect I might be missing a detail in how Next.js 14 handles cookies in server actions or there might be a specific configuration step I've overlooked.

Has anyone encountered this issue before, or does anyone have insights on how to properly modify cookies within server ponents in Next.js 14? Any help or pointers would be greatly appreciated.

Share Improve this question asked Jan 31, 2024 at 0:13 Léo LhuileLéo Lhuile 1032 silver badges4 bronze badges
Add a ment  | 

4 Answers 4

Reset to default 1

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

this message indicates that you should be on the server side but as far as I know axios.interceptors.response gets called immediately after axios receives a response from the server and before the promise returned by the Axios request is resolved.

so you are calling cookies.set after request is exectuted on the server

According to documentation you have to set cookies inside middleware if you are using NextJS 13 or newer version.

Good to know: `cookies()` is a Dynamic Function whose returned values cannot
be known ahead of time. Using it in a layout or page will opt a route into
dynamic rendering at request time.

After adding middleware you can read cookies anywhere you want but you have to mark the function as "use server".

Here is a YouTube video that explains better.

Here is the explanation that how i am doing this

For signout feature i am redirecting the page to /log-out and inside middleware i am checking the current url. If current url contains /log-out then i am clearing every cookies and then redirect to the main page.

import { NextResponse } from 'next/server';
import { NextRequest as NextRequestType } from 'next/server';

const ERROR_FALLBACK = "/log-out"

export async function middleware(req: NextRequestType) {
  let token = req.cookies.get("ACCESS_TOKEN");

  if (req.nextUrl.pathname.includes(ERROR_FALLBACK)) {
    token = undefined;
  }

  if (!token) {
    try {
      const initBody = {
        device: { osType: 'web' }
      };

      const { data: result } = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}${Endpoints.INIT}`, initBody);
      const { token } = result.data;

      let response: NextResponse

      if (req.nextUrl.pathname.includes(ERROR_FALLBACK)) {
        response = NextResponse.redirect(new URL('/', req.url));
      } else {
        response = NextResponse.next();
      }

      response.cookies.set('ACCESS_TOKEN', encrypt(token, HashAlgorithm.AES), {
        maxAge: 25 * 365 * 24 * 60 * 60 * 1000
      });

      return response;
    } catch (error) {
      return NextResponse.redirect(new URL(ERROR_FALLBACK, req.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/((?!api|static|.*\\..*|_next).*)",
};

This middleware function is called everytime before page is loading if current url matches with matcher.

For more information about matcher you can check the documentation.

Explanation of the above code

In my backend app every user has access token. By calling this endpoint it returns the token. Inside middleware i am sending a request to my backend if there is no token inside cookies.

If current endpoint contains my FALLBACK url then i am resetting by updating it.

But in your case you want to clear cookies. So you can do this like below.

  if (req.nextUrl.pathname.includes(ERROR_FALLBACK)) {
    let response: NextResponse = NextResponse.redirect(new URL('/auth/signin', req.url));
    response.cookies.delete('token');
    response.cookies.delete('refreshToken');
    response.cookies.delete('user');
    return response;
  }

How to redirect inside axios?

You need to send a message to ui and listen to it inside client ponent. Here is a explanation of how you can do this.

First write a helper function that emits a message and listener for this.

import { EventEmitter } from 'events';

const emitter = new EventEmitter();

export const navigateTo = (path: string) => {
  emitter.emit('navigate', path);
};

export const onNavigate = (listener) => {
  emitter.on('navigate', listener);
};

And now write a helper ponent that listens this event in client.

"use client"

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { onNavigate } from '@/lib/Events';

export default function EventListener({ children }) {
  const router = useRouter();

  useEffect(() => {
    const handleNavigation = (path: string) => {
      router.replace(path);
    };

    onNavigate(handleNavigation);
  }, [router]);

  return children;
}

After creating above ponent add it to your top ponent. In my case i added this ponent inside layout.tsx

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <EventListener>
          {children}
        </EventListener>
        <Toaster />
      </body>
    </html>
  );
}

Now you can update your signout() function like below

export const signout = () => {
  navigateTo(WebLink.ERROR_FALLBACK);
};

After this you can call signout function wherever you want.

How to set token to axios header?

You can read cookies wherever you want if you mark the ponent or function as "use server"

"use server"

export async function getToken() {
  const a = await getCookieValue(COOKIE_KEY.TOKEN);
  return a;
}

async function getCookieValue(key: string) {
  return cookies().get(key)?.value;
}

And axios requester object

export const Requester = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL,
  timeout: 20000,
});

Requester.interceptors.request.use(
  async (config) => {
    const token = await getToken();
    if (token) {
      config.headers['token'] = token;
    }
    return config;
  },
  (error) => {
    console.warn(error);
    return Promise.reject(error);
  },
);

Requester.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response?.data?.meta?.statusCode === 101) {
      endSession();
      return {};
    }

    if (error.response?.data?.meta?.statusCode === 104) {
      endSession();
      return {};
    }
    return Promise.reject(error);
  },
);

Importing cookies inplace Dynamically worked for me:

api.interceptors.response.use(
    (response) => response,
    async (error) => {
        const originalRequest = error.config;
        if (error.response.status === 400 && !originalRequest._retry) {
            //......
            const response = await axios.post(....)
            const { cookies } = await import('next/headers'); //<-----HERE
            const cookieInstance = await cookies();
            const cookieString = response.headers['set-cookie'] as any;
            const access = parseCookieHelpers(cookieString);
            if (access) cookieInstance.set(access.name, access.value, access.options as any);
                return await api.request(originalRequest);
            } catch (refreshError) {
                return Promise.reject(refreshError);
            }
        }
        return Promise.reject(error);
    }
);

The Next.js Cookie Documentation is a bit misleading with how you can actually access cookies.

You can apparently read cookies in a server ponent, but you cannot set them. I've found that trying to do anything with cookies within a server ponent, I was receiving the error that it must be in a Server Action or Route Handler.

I utilized useEffect() within a client ponent to be able to access & set cookies from my server action:

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

export default function Home() {

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

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

    }, [cookie]);

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

}

app/_actions/cookie.ts

"use server"
import { cookies } from "next/headers";

export async function getCookie() {
    const cookie = (await cookies()).get("cookie_name");

    return cookie?.value;
}

本文标签: javascriptNextjs 14 Server Actions Issue Modifying CookiesStack Overflow