next.js: [NEXT-1126] Cookies set in middleware missing on Server Component render pass

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

    Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.3.0: Thu Jan  5 20:48:54 PST 2023; root:xnu-8792.81.2~2/RELEASE_ARM64_T6000
    Binaries:
      Node: 18.13.0
      npm: 8.19.3
      Yarn: N/A
      pnpm: 7.25.0
    Relevant packages:
      next: 13.4.1
      eslint-config-next: 13.1.6
      react: 18.2.0
      react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true), Middleware / Edge (API routes, runtime)

Link to the code that reproduces this issue

https://codesandbox.io/p/sandbox/exciting-tamas-jhekzr

To Reproduce

  • Load the URL in your browser
  • Check logs. It should log a UUID newSessionId from middleware.ts, but undefined sessionId from layout.tsx.
  • If you refresh the page, sessionId is now picked up correctly and logged out from layout.tsx.

Describe the Bug

Cookies set in middleware only show up in Server Components from the first request after it was set.

Expected Behavior

I would expect a cookie set in middleware, to be available in Server Components in the render pass in which it was set.

Which browser are you using? (if relevant)

Version 112.0.5615.137 (Official Build) (arm64)

How are you deploying your application? (if relevant)

This happens both locally and on our Vercel preview environments

NEXT-1126

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 49
  • Comments: 54 (2 by maintainers)

Commits related to this issue

Most upvoted comments

This happens because cookies are read from the Cookies header on the request, whereas you set cookies using the Set-Cookie header on the response. For a single given page load, Middleware and Server Components executed as part of a single request/response cycle. When it receives a Set-Cookie header on a response, the browser can’t then go back in time to put a different Cookie header on the request it already sent (the reason the redirect workaround works is that it creates a second request, which will carry the updated Cookie header).


For cookies set in middleware, you can work around this problem using the mechanism added in https://github.com/vercel/next.js/pull/41380 — this allows middleware to “override” what Next will tell Server Components about the value of the Cookie header on the request. Overriding the Cookie header will make Server Component functions like cookies() return different values, even for the same request.

The following is an applySetCookie(req, res) function that you can drop in to your existing middlewares, which will make the cookies() function in RSC reflect the newly-set cookies as expected

import { NextResponse, type NextRequest } from 'next/server';
import { ResponseCookies, RequestCookies } from 'next/dist/server/web/spec-extension/cookies';


/**
 * Copy cookies from the Set-Cookie header of the response to the Cookie header of the request,
 * so that it will appear to SSR/RSC as if the user already has the new cookies.
 */
function applySetCookie(req: NextRequest, res: NextResponse): void {
  // parse the outgoing Set-Cookie header
  const setCookies = new ResponseCookies(res.headers);
  // Build a new Cookie header for the request by adding the setCookies
  const newReqHeaders = new Headers(req.headers);
  const newReqCookies = new RequestCookies(newReqHeaders);
  setCookies.getAll().forEach((cookie) => newReqCookies.set(cookie));
  // set “request header overrides” on the outgoing response
  NextResponse.next({
    request: { headers: newReqHeaders },
  }).headers.forEach((value, key) => {
    if (key === 'x-middleware-override-headers' || key.startsWith('x-middleware-request-')) {
      res.headers.set(key, value);
    }
  });
}

Example usage

middleware.ts
export default function middleware(req: NextRequest) {
  // Set cookies on your response
  const res = NextResponse.next();
  res.cookies.set('foo', 'bar');

  // Apply those cookies to the request
  applySetCookie(req, res);

  return res;
}
app/page.tsx
import { cookies } from 'next/headers';
export default function MyPage() {
  console.log(cookies().get('foo')); // logs “bar”
  // ...
}

Yeah I’m having the exact same issue.

Setting cookies for the first response is pretty critical for most web apps I’d imagine (i.e. session IDs), so hopefully there’s a way to make this work. I assume we can’t set cookies in the Server Component itself because Next has already begun streaming the response by the time it runs?

A hacky workaround for now: use a redirect to trigger a new request with the cookie attached

export function middleware(request) {
  let sid = request.cookies.get("sid")?.value;
  if (!sid) {
    let id = crypto.randomUUID();
    // @TODO Have to redirect here to ensure cookie is available to root layout
    let response = NextResponse.redirect(request.url);
    response.cookies.set("sid", id);
    return response;
  }
}

This is obviously not a great solution though since it adds a whole extra request for any visitors without the cookie (and could trigger an infinite loop if their browser blocks cookies).

@timneutkens with the App Router having settled at stable somewhat, might we ask you to reconsider the priority on this? It’s still a huge problem for us, and it seems like others in the thread here have a similar view on the criticality of this issue. I’m not trying to antagonize, just kindly asking to reconsider or let us know what we can expect here. Thanks ❤️

we’re gonna look at tackling this soon, thank you for your patience

Please be patient, there’s hundreds of open issues and this one is not the top priority over performance/memory issues and such. The issue is in our project for App Router followups.

@StevenLangbroek Fair, again I share your frustration. Several fairly standard and simple use cases in middleware involve setting a cookie that needs to be accessed inside a server component that should be covered by docs:

  • Setting a unique session ID
  • Refreshing an authorization token

It would be great to get an official voice on how to do it w/o relying on the community to figure it out.

For other folks reading this, please still test out the solution I posted above. I would like to get external validation that it works.

I’m interfacing with an API that I have no control over, but the expiry is set to 14 days, so @oliverjam’s redirect workaround (https://github.com/vercel/next.js/issues/49442#issuecomment-1538691004) is good enough for me and seems to be working well. I’d rather an awkward redirect instead of an error page once a fortnight.

Disappointing that something as basic as token refreshing has been essentially broken for almost a year now.

This is still not working, I guess the redirect option is the only working solution. Its 2024, I expected a working solution for a basic use case like this before calling it stable.

You could also try this workaround that doesn’t require any changes to middleware.

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

export default function middleware() {
  const response = NextResponse.next();

  if (request.cookies.has("test")) {
    console.log("middleware: delete test");
    response.cookies.delete("test");
  } else {
    const value = crypto.randomUUID();
    console.log("middleware: create test", value);
    response.cookies.set({
      name: "test",
      value: value,
      maxAge: 30,
    });
  }

  return response;
}

// page.tsx or layout.tsx
import { getCookie } from "cookie-util"; 

export default async function Test() {
  const test = getCookie("test");
  console.log("page:", test);
  return <p>{JSON.stringify(test)}</p>;
}

// cookie-util.ts
import {
  RequestCookie,
  parseCookie,
} from "next/dist/compiled/@edge-runtime/cookies";
import { cookies, headers } from "next/headers";

export function getCookie(cookieName: string): RequestCookie | undefined {
  const allCookiesAsString = headers().get("Set-Cookie");

  if (!allCookiesAsString) {
    return cookies().get(cookieName);
  }

  const allCookiesAsObjects = allCookiesAsString
    .split(", ")
    .map((singleCookieAsString) => parseCookie(singleCookieAsString.trim()));

  const targetCookieAsObject = allCookiesAsObjects.find(
    (singleCookieAsObject) =>
      typeof singleCookieAsObject.get(cookieName) == "string",
  );

  if (!targetCookieAsObject) {
    return cookies().get(cookieName);
  }

  return {
    name: cookieName,
    value: targetCookieAsObject.get(cookieName) ?? "",
  };
}

// output
middleware: create test f54a058f-fcb8-47ab-b2c2-cf42e42f4c17
page: { name: 'test', value: 'f54a058f-fcb8-47ab-b2c2-cf42e42f4c17' }
middleware: delete test
page: { name: 'test', value: '' }

None of the workarounds work when deployed to cloud env, esp. when the RSC is on runtime = 'nodejs' and the middleware is on Edge (as it is always on Vercel).

The headers() and cookies() helper magic methods are supposed to return only the REQUEST headers/cookies, while set-cookie is a response header.

Workaround (Vercel-friendly and validated)

The “correct” way of thinking and working around that limitation is to do one of the two things:

  1. If you must have access to the data in the flow of a single request, then you must share state between middleware and RSC - i.e. use unstable_cache(), redis, memcached etc.
  2. or split the job into two requests with a redirect:
// middleware.ts
export default async function (req: NextRequest) {
  if (needsToAddACookie) {
    const response = NextResponse.redirect(req.nextUrl.clone()); // <- redirect back to same url
    response.cookies.set({
      name: "something",
      value: "important",
      maxAge: 100,
    });
    return response;
  }
}
// page.tsx
export default function MyPage() {
  const somethingImportant = cookies().get('something')?.value;
}

In middleware, set coockie works localy (both in dev and prod) without any fix or workaround. But when in Vercel deployement it still doesn’t work even with the fix :

     // Apply those cookies to the request (Set-Cookie header of the response to the Cookie header of the request)
    applySetCookie(request, res); 

Any idea when this will be fixed please ?

Another +1 here, the issue isn’t strictly with Server Components, page routing has the same problems.

  1. Set cookies in middleware (as per docs)
  2. Return response in middleware (as per docs)
  3. Try to access that cookie in getServerSideProps <- cookie isn’t available here

It’s not uncommon to need to ensure the presence of a cookie value such as session ID’s

Hi,

I attempted your fix and after the redirect i’m still not seeing the cookie in the front end. Which redirects me back to the login. Only when the application is deployed though, it doesn’t happen when using it locally.

import { jwtVerify, importSPKI } from "jose";
import {
	ResponseCookies,
	RequestCookies,
} from "next/dist/server/web/spec-extension/cookies";
import { NextResponse, NextRequest } from "next/server";

/**
 * Copy cookies from the Set-Cookie header of the response to the Cookie header of the request,
 * so that it will appear to SSR/RSC as if the user already has the new cookies.
 */
function applySetCookie(req: NextRequest, res: NextResponse): void {
	// parse the outgoing Set-Cookie header
	const setCookies = new ResponseCookies(res.headers);
	// Build a new Cookie header for the request by adding the setCookies
	const newReqHeaders = new Headers(req.headers);
	const newReqCookies = new RequestCookies(newReqHeaders);
	setCookies.getAll().forEach((cookie) => newReqCookies.set(cookie));
	// set “request header overrides” on the outgoing response
	NextResponse.next({
		request: { headers: newReqHeaders },
	}).headers.forEach((value, key) => {
		if (
			key === "x-middleware-override-headers" ||
			key.startsWith("x-middleware-request-")
		) {
			res.headers.set(key, value);
		}
	});
}

export async function middleware(request: NextRequest) {
	const jwt = request.cookies.get("auth_token");
	if (jwt) {
		try {
			const secret = Buffer.from(process.env.SECRET, "base64");
			const key = await importSPKI(secret.toString(), "RS256");
			await jwtVerify(jwt.value, key);

			if (request.url.includes("/login")) {
				const response = NextResponse.redirect(new URL("/", request.url));
				response.cookies.set("auth_token", jwt.value, {
					path: "/",
					sameSite: "none",
					secure: true,
				});
				applySetCookie(request, response);
				return response;
			}

			const response = NextResponse.next();
			response.cookies.set("auth_token", jwt.value, {
				path: "/",
				sameSite: "none",
				secure: true,
			});
			applySetCookie(request, response);
			return response;
		} catch (e) {
			console.log(e);
			request.cookies.delete("auth_token");
			return NextResponse.redirect(new URL("/login", request.url));
		}
	} else {
		if (request.url.includes("/login")) {
			return NextResponse.next();
		}
		return NextResponse.redirect(new URL("/login", request.url));
	}
}

Does anyone still encounter this issue?

Issue also applies to headers set on NextResponse.next() after construction:

// middleware.ts

// this returns NextResponse.next(), but I've also tried constructing it myself (which produced the same result)
const res = intlMiddleware(req); 

res.headers.set('Session-ID', crypto.randomUUID());

// layout.tsx
console.log({
  locale: cookies().get("NEXT_LOCALE")?.value, // undefined
  header: headers().get("Session-ID"), // null
});

I can’t pass these to the NextReponse.next static method, as next-intl is in charge of that.

edit: I was testing this incorrectly, headers work actually, apologies for any confusion. the issue with cookies remains.

Kyle Steven Langbroek Could you also please test the above fix?

Sorry I accidentally deleted a reply of mine, but just to reiterate: your solution was mentioned before, and did / does not work on production.

@kylekz @StevenLangbroek Could you also please test the above fix?

Honestly, I’ve moved on from the project where I need this, and have stopped using and recommending NextJS or Vercel. If the company acting as its steward can’t prioritize a bugfix over [insert swanky AI feature], I’m putting my teams and their commitments at risk.

@controversial @andre-muller I think I finally found out the fix which took me too many hours.

https://nextjs.org/docs/app/api-reference/functions/next-response#next

Cookies are read-only in server components and allow you to read incoming HTTP request cookies. However, when you set-cookie, it modifies the set-cookie header on the HTTP response.

For the incoming request to contain your newly set cookie, you can add it to the incoming request.

const setCookie = (request: NextRequest, response: NextResponse, cookie: ResponseCookie) => {
  request.cookies.set(cookie)
  response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })
  response.cookies.set(cookie)
  return response;
}

export async function middleware(request: NextRequest, response: NextResponse) {
  response = setCookie(request, response, { name: 'cookieName', value: 'cookieValue' })
  return response;
}

Even though it took me many hours, I would ask the NextJS team to make this more clear in the documentation. The pattern is quite unintuitive.

Hi,

I attempted your fix and after the redirect i’m still not seeing the cookie in the front end. Which redirects me back to the login. Only when the application is deployed though, it doesn’t happen when using it locally.

import { jwtVerify, importSPKI } from "jose";
import {
	ResponseCookies,
	RequestCookies,
} from "next/dist/server/web/spec-extension/cookies";
import { NextResponse, NextRequest } from "next/server";

/**
 * Copy cookies from the Set-Cookie header of the response to the Cookie header of the request,
 * so that it will appear to SSR/RSC as if the user already has the new cookies.
 */
function applySetCookie(req: NextRequest, res: NextResponse): void {
	// parse the outgoing Set-Cookie header
	const setCookies = new ResponseCookies(res.headers);
	// Build a new Cookie header for the request by adding the setCookies
	const newReqHeaders = new Headers(req.headers);
	const newReqCookies = new RequestCookies(newReqHeaders);
	setCookies.getAll().forEach((cookie) => newReqCookies.set(cookie));
	// set “request header overrides” on the outgoing response
	NextResponse.next({
		request: { headers: newReqHeaders },
	}).headers.forEach((value, key) => {
		if (
			key === "x-middleware-override-headers" ||
			key.startsWith("x-middleware-request-")
		) {
			res.headers.set(key, value);
		}
	});
}

export async function middleware(request: NextRequest) {
	const jwt = request.cookies.get("auth_token");
	if (jwt) {
		try {
			const secret = Buffer.from(process.env.SECRET, "base64");
			const key = await importSPKI(secret.toString(), "RS256");
			await jwtVerify(jwt.value, key);

			if (request.url.includes("/login")) {
				const response = NextResponse.redirect(new URL("/", request.url));
				response.cookies.set("auth_token", jwt.value, {
					path: "/",
					sameSite: "none",
					secure: true,
				});
				applySetCookie(request, response);
				return response;
			}

			const response = NextResponse.next();
			response.cookies.set("auth_token", jwt.value, {
				path: "/",
				sameSite: "none",
				secure: true,
			});
			applySetCookie(request, response);
			return response;
		} catch (e) {
			console.log(e);
			request.cookies.delete("auth_token");
			return NextResponse.redirect(new URL("/login", request.url));
		}
	} else {
		if (request.url.includes("/login")) {
			return NextResponse.next();
		}
		return NextResponse.redirect(new URL("/login", request.url));
	}
}

Does anyone still encounter this issue?

Yeah when using locally i can see the header, but when i deploy it is not working. @controversial Do you have any idea about it ?

@StevenLangbroek Can you link to where it was mentioned? My solution is setting request.cookies.set, I do not see it mentioned anywhere in this thread.

While I agree that the lack of official comment is frustrating, there are still folks trying to resolve this issue so it would be great, if there is any working solution, to document it.

I appreciate and value that effort, I’m just no longer interested in contributing, given the lack of interest by the owner of the project and the fact that I’m not currently using it. I’m sure others will gladly help you test your solution. Good luck!

The feature to override request headers from Middleware is described in the docs as part of the official API, so it should be stable, but of course “should be” != “is”

The workaround I posted worked at the time that I wrote it, but it’s possible that this feature has changed/broken in more recent Next.js releases.

@controversial’s workaround works in dev mode but not in the prod build. I’m using pages router with Next v14.0.4. Cookies set on request or response don’t appear in the getServerSideProps in prod build.

I’m sorry, friends. I’d like to set the record straight. According to the documentation we can only set or delete cookies in the

  1. middleware
  2. server actions
  3. route handler. After reading this thread I realized that setting or removing cookies in middleware does not do it properly (currently there is a bug). Server actions must be triggered from the client side.

What should I do if I want to handle a normal simple authorization case? Let’s say, in the server component, I realize that the jwt token that is stored in the cookie is out of date (it could be a 401 response when receiving data or just checking the token and realizing it’s old) and I want to delete it. How do I do it correctly? According to the documentation, you can’t delete the cookie in the server component. I could delete it in middleware but that doesn’t give the proper result either. I’ve been trying to solve this problem for quite a while now, but I haven’t figured out how the nextjs team recommends doing it. I would be grateful for any information or help.

@timneutkens this is starting to block our work, is there any estimate attached to the Linear issue? The suggested workaround doesn’t work for us. If I construct ResponseCookies myself with the return value of headers(), it’s entirely empty.