Forwarding cookies from Server Components to Route Handlers with Next.js App Router

9th July 2023
Jon Meyers profile pic
Jon Meyers @jonmeyers_io

In this video about implementing cookie-based auth with Next.js Route Handers, I claimed there was a bug where Server Components were not correctly piping through cookies to the GET method of Route Handlers.

Turns out my mental model was completely wrong! Next.js does not have a bug and this behaviour is totally expected! 🤦‍♂️

TLDR; We need to manually attach the headers from the original request to the fetch call made from the Server Component 👇

import { headers } from "next/headers";

export default async function Page() {
  await fetch("http://localhost:3000/test", {
    headers: headers(),
  });

  return ...
}
  

The problem is that the cookie — an auth token, for example — is set between our browser and the server. When our browser requests a Server Component route, it sends the cookies along with the request:

Browser → Server Component

If the Server Component makes a fetch call to a Route Handler, it doesn ‘t know to attach the original request ‘s headers and cookies.

Browser → Server Component → Route Handler

If we want them to be available in the Route Handler, we need to manually attach them to the fetch request from the Server Component.

To step through this problem, add a new file at app/test/route.ts to your Next.js App Router project, with the following content:

import { cookies } from "next/headers";

export async function GET() {
  console.log(cookies().getAll());
  return new Response("working");
}
  

This will console log any cookies that are passed to this Route Handler.

Now create a Server Component that calls this Route Handler before rendering the page:

export default async function Page() {
  await fetch("http://localhost:3000/test");
  return <h1>Hello</h1>;
}
  

When loading this route, we get an empty array printed out to the console. This is what we expect as we don ‘t yet have any cookies. Let ‘s add a 🍪!

Extend the test Route Handler to handle POST requests, and use this to set a cookie:

export async function POST() {
  cookies().set("is-jon-cool", "absolutely");
  return new Response("working");
}
  

The whole Route Handler now looks like this:

import { cookies } from "next/headers";

export async function GET() {
  console.log(cookies().getAll());
  return new Response("working");
}

export async function POST() {
  cookies().set("is-jon-cool", "absolutely");
  return new Response("working");
}
  

Let ‘s call this POST handler when we click a button - we will need a Client Component for this.

Create a new file called client.tsx and populate with the following:

"use client";

export default function Client() {
  const handleCookie = async () => {
    await fetch("http://localhost:3000/test", {
      method: "post",
    });
  };

  return <button onClick={handleCookie}>Set cookie</button>;
}
  

Update our Server Component Route to render this Client Component.

import Client from "./client";

export default async function Page() {
  await fetch("http://localhost:3000/test");
  return <Client />;
}
  

Now when we click that button it sets a new cookie, but when we refresh the page — triggering the GET request from our Server Component to the Route Handler — we expect to see our new cookie value printed to the console, but again we see an empty array!

What is going on?

The problem is when we click the button in our Client Component, it is making a POST request from the browser to the Route Handler. This sets our new cookie in the browser.

Browser → Route Handler

When we refresh the page, a request is made from the browser to our Server Component route — this has the cookie attached.

Browser → Server Component

But then our Server Component is making another request to our Route Handler, and by default it doesn ‘t forward any of our headers from the browser ‘s request.

Browser → Server Component → Route Handler

So all we need to do to fix this is pass those headers along from our Server Component to the Route Handler.

import Client from "./client";
import { headers } from "next/headers";

export default async function Page() {
  await fetch("http://localhost:3000/test", {
    headers: headers(),
  });

  return <Client />;
}
  

Now if we refresh we will see our delicious cookie printed out to the console! 🚀