vercel / next.js

The React Framework
https://nextjs.org
MIT License
127.23k stars 27.02k forks source link

Docs: Setting cookies in API Route Handlers isn't working at all. Unlike in middleware #52799

Closed wkd-kapsule closed 1 year ago

wkd-kapsule commented 1 year ago

What is the improvement or update you wish to see?

The current doc on how to set a cookie in the route handler using the app router is wrong. No matter what I try, I never get to set the cookie. Yet, the same method works in the middleware function.

Is there any context that might help us understand?

Help please

Does the docs page already exist? Please link to it.

https://nextjs.org/docs/app/building-your-application/routing/router-handlers#cookies

DX-1790

asp3 commented 1 year ago

for me, setting cookies is working as expected, but when calling getting cookies from a route handler, there are no cookies (null)

ResponseCookies {
  _parsed: Map(0) {},
  _headers: HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(0) {},
    [Symbol(headers map sorted)]: []
  }
}
corvallo commented 1 year ago

I'm having the same issue.

Under /app/page.tsx, I have

export async function getConfig() {
  const configRes = await fetch('http://localhost:4200/api/config', {
    credentials: 'include', // cross-origin cookies
  });
  return await configRes.json();
}
export default async function Index() {
  const config = await getConfig();
  return <div>Index Page {JSON.stringify(config)}</div>;
}

Under /app/api/config/route.ts


  import { cookies } from 'next/headers';
  import { NextResponse } from 'next/server';
  export async function GET(req: Request) {
    // @ts-ignore
    cookies().set({
      name: 'cookieName',
      value: 'true',
      httpOnly: true,
    });
    return new NextResponse(
      JSON.stringify({
        t: 'test',
      }),
      { status: 200 },
    );
    //   const response = NextResponse.json(
    //     { data: 'config' },
    //     {
    //       status: 200,
    //     },
    //   );

    //   response.cookies.set('login', 'true', {
    //     path: '/',
    //     httpOnly: false,
    //     secure: true,
    //     maxAge: 60 * 60 * 24 * 7,
    //     sameSite: 'none',
    //   });
    //   return response;
}

I tried both commented and uncommented in route.ts, but none of this is working Moreover if I try to set cookie via server actions it works correctly.

wkd-kapsule commented 1 year ago

for me, setting cookies is working as expected, but when calling getting cookies from a route handler, there are no cookies (null)

ResponseCookies {
  _parsed: Map(0) {},
  _headers: HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(0) {},
    [Symbol(headers map sorted)]: []
  }
}

Would you please provide the code you used to set the cookie?

wkd-kapsule commented 1 year ago

I'm having the same issue.

Under /app/page.tsx, I have

export async function getConfig() {
  const configRes = await fetch('http://localhost:4200/api/config', {
    credentials: 'include', // cross-origin cookies
  });
  return await configRes.json();
}
export default async function Index() {
  const config = await getConfig();
  return <div>Index Page {JSON.stringify(config)}</div>;
}

Under /app/api/config/route.ts


  import { cookies } from 'next/headers';
  import { NextResponse } from 'next/server';
  export async function GET(req: Request) {
    // @ts-ignore
    cookies().set({
      name: 'bet9ja',
      value: 'true',
      httpOnly: true,
    });
    return new NextResponse(
      JSON.stringify({
        t: 'test',
      }),
      { status: 200 },
    );
    //   const response = NextResponse.json(
    //     { data: 'config' },
    //     {
    //       status: 200,
    //     },
    //   );

    //   response.cookies.set('login', 'true', {
    //     path: '/',
    //     httpOnly: false,
    //     secure: true,
    //     maxAge: 60 * 60 * 24 * 7,
    //     sameSite: 'none',
    //   });
    //   return response;
}

I tried both commented and uncommented in route.ts, but none of this is working Moreover if I try to set cookie via server actions it works correctly.

I'm right there with you... I tried both and even setting a Set-Cookie header, none worked. Btw, I think that if you update Typescript to the latest version you won't need these @ts-ignore anymore.

corvallo commented 1 year ago

I noticed that the fetch response has set-cookie inside the headers

Response {
  [Symbol(realm)]: { settingsObject: {} },
  [Symbol(state)]: {
    aborted: false,
    rangeRequested: false,
    timingAllowPassed: false,
    requestIncludesCredentials: false,
    type: 'default',
    status: 200,
    timingInfo: null,
    cacheState: '',
    statusText: '',
    headersList: HeadersList {
      [Symbol(headers map)]: [Map],
      [Symbol(headers map sorted)]: null
    },
    urlList: [],
    body: { stream: undefined, source: [Uint8Array], length: 12 }
  },
  [Symbol(headers)]: HeadersList {
    [Symbol(headers map)]: Map(7) {
      'connection' => 'close',
      'content-encoding' => 'gzip',
      'content-type' => 'text/plain;charset=UTF-8',
      'date' => 'Tue, 18 Jul 2023 07:32:30 GMT',
      'set-cookie' => 'cookieName=true; Path=/; HttpOnly',
      'transfer-encoding' => 'chunked',
      'vary' => 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding'
    },
    [Symbol(headers map sorted)]: null
  }
}
wkd-kapsule commented 1 year ago

Yes, the set-cookie header is shown when I log the headers and yet, no cookie...

corvallo commented 1 year ago

I made some further tests.

I created another api route under pages folder pages/api/testapi.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { serialize } from 'cookie';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const cookie = serialize('hello-cookie', 'api-hello-cookie-value', {
    path: '/',
  });
  res.setHeader('Access-Control-Expose-Headers', '*');
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:4200');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  //   res.setHeader('Set-Cookie', `token1=token1`);
  res.setHeader('Set-Cookie', cookie);
  res.status(200).json({ name: 'John Doe' });
}

I noticed that pointing the browser to http://localhost:4200/api/testapi, the cookie is correctly set and it is available to whole application. But if use that api endpoint within a fetch inside a page

export async function getConfig() {
  const configRes = await fetch('http://localhost:4200/api/prova');
  console.log(configRes);
  return await configRes.json();
}
export default async function Index() {
  await getConfig();
  return <div>Index Page </div>;
}

it doesn't work

corvallo commented 1 year ago

I made an almost complete codesandbox here https://codesandbox.io/p/sandbox/silly-cookies-vhlvvn check it out. I created a server action to show that the cookie is setted correctly while using server actions

balazsorban44 commented 1 year ago

Hi everyone, this is the expected behavior, but we should explain it better in the docs.

You cannot set cookies during render (inside Pages, Layouts, etc.). When a fetch request is made (eg. via a Route Handler), the Set-Cookie header won't be accounted for during the rendering. Route Handlers do support setting cookies which is documented here.

You can verify this by directly visiting an endpoint that uses Route Handlers. (Observation here https://github.com/vercel/next.js/issues/52799#issuecomment-1639831773)

When using Server Actions, the recommendation is to import the logic of the Route Handler instead of doing a fetch call, since you can set cookies via cookies().set inside a Server Action. See: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#using-headers

wkd-kapsule commented 1 year ago

Hi everyone, this is the expected behavior, but we should explain it better in the docs.

You cannot set cookies during render (inside Pages, Layouts, etc.). When a fetch request is made (eg. via a Route Handler), the Set-Cookie header won't be accounted for during the rendering. Route Handlers do support setting cookies which is documented here.

You can verify this by directly visiting an endpoint that uses Route Handlers. (Observation here #52799 (comment))

When using Server Actions, the recommendation is to import the logic of the Route Handler instead of doing a fetch call, since you can set cookies via cookies().set inside a Server Action. See: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#using-headers

Hi, thanks for the answer. Yet, it is still confusing to me.

The link you've provided to set a cookie in a Route Handler is using server action, which I'm not because it is still experimental. Other than that, how am I supposed to access/trigger that cookie function in the Route Handler if I can't fetch the Route? You talk about "directly visiting an endpoint". Am I supposed to redirect my users to a blank /api page just for the cookie to be set? This looks like a wobbly, barely effective workaround.

But I guess it doesn't even matter. Because my use case is involving a fetch request. During that fetch, depending on the data obtained, I want to set a corresponding cookie. Given what you said, Nextjs is intentionally built to make it impossible...

wkd-kapsule commented 1 year ago

"You can set cookies using Route Handlers" "The way to access a Route Handler is a fetch request" "You can't set a cookie using a fetch request and it is an intended behavior"

Isn't it a bit conflicting or am I getting something wrong?

rexfordessilfie commented 1 year ago

Hello @kapsule-studio. Not sure if this is already been suggested regarding the primary issue, but I think the issue is that when rendering your page, the fetch call you make inside of getConfig occurs on the server-side, where the browser cannot receive and store the cookies from the headers.

Some solutions you may try:

  1. The suggestion @balazsorban44 is making is to copy over similar logic as you have inside of your route handler into the server action, rather than making a fetch request. According to the docs, the cookies().set method should appropriately set the cookies to be returned to the browser and stored by the browser once the page is rendered. One thing I found trying this was to trigger the server action via a form submission.

  2. An alternate potential solution to try is to opt out of the server-side rendering into client-side rendering and use something such as a useEffect to make the fetch request client-side, this way the response object would make its way back to the browser where it can set the cookies appropriately. For this, you may have to use credentials: 'include' in the fetch request.

Both of these approaches are in this CodeSandbox example. You may have to run them locally to notice cookies being set. I did not see them getting set in the CodeSandbox environment.

aalfiann commented 1 year ago

I've read the docs and tried many times but still fail, cookies still undefined..

How to get cookies with use client in page.tsx? The docs about cookies is not complete..

Update:
Finally I've made it, with using route and call it with

fetch('http://localhost:3000/api/session', { cache: 'no-store' })
ARGONoid commented 1 year ago

I keep getting this error Error: Cookies can only be modified in a Server Action or Route Handler.

Is possible set cookie on server side? My code

'use server'

import { cookies } from 'next/headers'

async function deleteCookie(name: string) {
  cookies().delete(name)
}

This function is called in the server component. I can't set cookies on the client side because httpOnly is required for authorization.

AayushKarki714 commented 1 year ago

Hi everyone, this is the expected behavior, but we should explain it better in the docs.

You cannot set cookies during render (inside Pages, Layouts, etc.). When a fetch request is made (eg. via a Route Handler), the Set-Cookie header won't be accounted for during the rendering. Route Handlers do support setting cookies which is documented here.

You can verify this by directly visiting an endpoint that uses Route Handlers. (Observation here #52799 (comment))

When using Server Actions, the recommendation is to import the logic of the Route Handler instead of doing a fetch call, since you can set cookies via cookies().set inside a Server Action. See: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#using-headers

Ohh, I got it, sending a request from client to server only sent the cookies, but making a api call during the render phase inside of server component won't send the cookies, since the client is the one who has the cookies, but in this case we are dealing with server to server

AayushKarki714 commented 1 year ago

Is there anyway to make sure that cookies is sent even in the api call made inside server components??

achievecreative commented 1 year ago

I keep getting this error Error: Cookies can only be modified in a Server Action or Route Handler.

Is possible set cookie on server side? My code

'use server'

import { cookies } from 'next/headers'

async function deleteCookie(name: string) {
  cookies().delete(name)
}

This function is called in the server component. I can't set cookies on the client side because httpOnly is required for authorization.

I have same issue in 14.0.1, the doc said we can set the cookie in server action - https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#setting-cookies

leerob commented 1 year ago

You should be able to set cookies in both Server Actions and Route Handlers. The documentation is correct. If you are seeing an issue, please open a new issue with a reproduction so we can investigate your case. Thanks!

github-actions[bot] commented 1 year ago

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.