typescriptnext.jscookiesnext.js13server-action

Trouble with Server Actions in Next.js v14 - Cookies Modification Error


I'm working with Next.js v14 and encountering an issue with Server Actions, specifically when trying to modify cookies. Despite following the documentation and ensuring that my functions are Server Actions, I'm still facing an error. Here's a brief overview of my setup and the problem:

Component (src/app/page.tsx):

import React from 'react';
import { cookies } from 'next/headers';
import { exampleAction } from './actions';

const serverActionCookies = async () => {
    'use server';
    cookies().set('name', 'Delba');
    cookies().delete('name');
};

const Test = async () => {
    await exampleAction();
    await serverActionCookies();
    return null;
};

const Page = () => (
    <>
        <Test />
        Test server action
    </>
);

export default Page;

Action (src/app/actions.ts):

'use server';

import { cookies } from 'next/headers';

export async function exampleAction() {
    cookies().set('name', 'Delba');
    cookies().delete('name');
}

When I try to run this, neither exampleAction nor serverActionCookies work as expected. I receive the following error:

Error: Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options

Both functions are intended to be Server Actions, yet they seem not to be recognized as such. I've checked the documentation and my code structure, but I can't figure out what I'm missing or doing wrong.

Thanks in advance !


Solution

  • The Why:

    ...HTTP does not allow setting cookies after streaming starts, so you must use .set() in a Server Action or Route Handler.

    The problem with setting cookies is how an HTTP response runs and React's new feature called Streaming.

    When a Nextjs server component runs it has already created the HTTP response which is composed of a response header and the body (https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#headers). We have to remember that once the HTTP response starts, it's already rendered the HTML or data it's going to send to the browser in the response body. This means any server methods that would have run (setting a server cookie) would have run before the HTTP response was built.

    The big gotcha is how React streaming works: https://www.patterns.dev/react/streaming-ssr/ - in a nutshell

    Like progressive hydration, streaming is another rendering mechanism that can be used to improve SSR performance. As the name suggests, streaming implies chunks of HTML are streamed from the node server to the client as they are generated.

    So because the HTTP request has already been built and if a slow async sub-component tries to set a cookie on the server it can't because it's now being streamed on the client (I equate it to an AJAX request bringing in HTML from the server).

    Let's say you have 20 slow components, React sends the prerendered HTML + the Suspense fallback HTML.

    <FooBarComponent />
    <Suspense fallback={<div>Loading...</div>}>
       <FreshlyLoadedContent />
    </Suspense>
    

    HTTP response renders this:

    <div>foo bar</div>
    <div id="8:0">Loading...</div>
    

    Any remaining streamed HTML is done on the client. React replaces chunks of HTML (it uses an id="" on each streamed component) one at a time in the DOM once the slow asynchronous components complete their requests and rendering cycles. This is why you see some of the pages with a loading state (Loading...) and other parts already rendered.

    Once streaming of that component is complete it replaces the contents of the div with something like this:

    <div>foo bar</div>
    <div id="8:0">Fresly loaded content!</div>
    

    The big up for this is...

    ...As the client starts receiving “bytes” of HTML earlier even for large pages, the TTFB is reduced and relatively constant. All major browsers start parsing and rendering streamed content or the partial response earlier. As the rendering is progressive, it results in a fast FP and FCP.

    So as a result,

    https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#behavior - It says:

    Server actions can be invoked using the action attribute in a element:

    and

    Server Actions are not limited to and can be invoked from event handlers, useEffect, third-party libraries, and other form elements like .

    This means that Server Actions can only be invoked from client-based components. Think of posting a form to the server or calling a secure server-based API method to send sensitive Credit Card information. Make sense?

    So in this case you need to create a client component and invoke your server action exampleAction() perhaps using useEffect or some client-based action (i.e. onClick). This will then invoke the server to do a server-based action from the client. This is one way to get around this.

    You can also use middleware where you can both read and set a cookie before NextJS starts the HTTP response back to the client and any streaming begins:

    import { NextRequest, NextResponse } from "next/server";
    
    export function middleware(request){
      let response = NextResponse.next()
      response.cookies.set("foo", "bar")
    
      return response;
    }