[GH-ISSUE #8464] nextjs cookie plugin causes router refresh #11097

Closed
opened 2026-04-13 07:28:38 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @ping-maxwell on GitHub (Mar 6, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8464

Originally assigned to: @ping-maxwell on GitHub.

Note: This is opened for tracking purposes.

Discord: https://discord.com/channels/1288403910284935179/1476968169762586747

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

calling cookies.set in a server actions invalidates client router cache and forces a refetch. This refetches our pages and causes a continuous “loop” forever refetching the page

Current vs. Expected behavior

current:
router refresh due to cookie set
expected:
no router refreshes

What version of Better Auth are you using?

1.5.4

System info

not applicable

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @ping-maxwell on GitHub (Mar 6, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8464 Originally assigned to: @ping-maxwell on GitHub. ## Note: This is opened for tracking purposes. Discord: https://discord.com/channels/1288403910284935179/1476968169762586747 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce calling cookies.set in a server actions invalidates client router cache and forces a refetch. This refetches our pages and causes a continuous “loop” forever refetching the page ### Current vs. Expected behavior current: router refresh due to cookie set expected: no router refreshes ### What version of Better Auth are you using? 1.5.4 ### System info ```bash not applicable ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the platformbug labels 2026-04-13 07:28:39 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 6, 2026):

This is a known behavior caused by Next.js's built-in cache invalidation - any call to cookies().set() in Server Actions automatically triggers router cache revalidation. The nextCookies plugin calls Next.js's native cookies API for each Set-Cookie header from auth responses, which triggers this behavior.

A related issue has been reported: when endpoints delete an existing session cookie and set a new one sequentially, it causes multiple page refreshes, creating a brief window where getSession returns null and potentially causing redirect loops.

Possible workarounds:

  1. For session refresh issues specifically, you can disable refresh in server-side calls with query: { disableRefresh: true } and let client-side useSession handle refreshes instead
  2. Place a client component that calls authClient.getSession() in your root layout to handle session refresh on navigation rather than in Server Actions

Could you share more details about which specific auth operation is triggering this? That would help identify if there's a more targeted fix available.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4014275131 --> @dosubot[bot] commented on GitHub (Mar 6, 2026): <!-- Answer --> This is a known behavior caused by Next.js's built-in cache invalidation - any call to `cookies().set()` in Server Actions automatically triggers router cache revalidation. The [nextCookies plugin calls Next.js's native cookies API](https://github.com/better-auth/better-auth/blob/7fefc2bbed2b66b2349ca182144b10eff171e345/packages/better-auth/src/integrations/next-js.ts#L92-L106) for each Set-Cookie header from auth responses, which triggers this behavior. A related issue has been reported: [when endpoints delete an existing session cookie and set a new one sequentially, it causes multiple page refreshes](https://github.com/better-auth/better-auth/issues/6077#issuecomment-3549443508), creating a brief window where `getSession` returns `null` and potentially causing redirect loops. **Possible workarounds:** 1. For session refresh issues specifically, you can [disable refresh in server-side calls with `query: { disableRefresh: true }`](https://github.com/better-auth/better-auth/issues/2115#issuecomment-3487943692) and let client-side `useSession` handle refreshes instead 2. Place a client component that calls `authClient.getSession()` in your root layout to handle session refresh on navigation rather than in Server Actions Could you share more details about which specific auth operation is triggering this? That would help identify if there's a more targeted fix available. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/73c05333-216e-4f2f-8f6a-ebfa99fa6699?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20behavior%20caused%20by%20Next.js%27s%20built-in%20cache%20invalidation%20-%20any%20call%20to%20%60cookies%28%29.set%28%29%60%20in%20Server%20Actions%20automatically%20triggers%20router%20cache%20revalidation.%20The%20%5BnextCookies%20plugin%20calls%20Next.js%27s%20native%20cookies%20API%5D%28https%3A//github.com/better-auth/better-auth/blob/7fefc2bbed2b66b2349ca182144b10eff171e345/packages/better-auth/src/integrations/next-js.ts%23L92-L106%29%20for%20each%20Set-Cookie%20header%20from%20auth%20responses%2C%20which%20triggers%20this%20behavior.%0A%0AA%20related%20issue%20has%20been%20reported%3A%20%5Bwhen%20endpoints%20delete%20an%20existing%20session%20cookie%20and%20set%20a%20new%20one%20sequentially%2C%20it%20causes%20multiple%20page%20refreshes%5D%28https%3A//github.com/better-auth/better-auth/issues/6077%23issuecomment-3549443508%29%2C%20creating%20a%20brief%20window%20where%20%60getSession%60%20returns%20%60null%60%20and%20potentially%20causing%20redirect%20loops.%0A%0A%2A%2APossible%20workarounds%3A%2A%2A%0A1.%20For%20session%20refresh%20issues%20specifically%2C%20you%20can%20%5Bdisable%20refresh%20in%20server-side%20calls%20with%20%60query%3A%20%7B%20disableRefresh%3A%20true%20%7D%60%5D%28https%3A//github.com/better-auth/better-auth/issues/2115%23issuecomment-3487943692%29%20and%20let%20client-side%20%60useSession%60%20handle%20refreshes%20instead%0A2.%20Place%20a%20client%20component%20that%20calls%20%60authClient.getSession%28%29%60%20in%20your%20root%20layout%20to%20handle%20session%20refresh%20on%20navigation%20rather%20than%20in%20Server%20Actions%0A%0ACould%20you%20share%20more%20details%20about%20which%20specific%20auth%20operation%20is%20triggering%20this%3F%20That%20would%20help%20identify%20if%20there%27s%20a%20more%20targeted%20fix%20available.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/8464)
Author
Owner

@ForrestDevs commented on GitHub (Mar 24, 2026):

Any updates on this? Just went a removed nextCookies plugin for the time being.

<!-- gh-comment-id:4114656460 --> @ForrestDevs commented on GitHub (Mar 24, 2026): Any updates on this? Just went a removed nextCookies plugin for the time being.
Author
Owner

@madaxen86 commented on GitHub (Mar 25, 2026):

Any updates on this? Just went a removed nextCookies plugin for the time being.

Caused me some headache today, too. Thanks for the great payload-auth package.
Sidenote: better-auth@1.4 works with nextCookies

<!-- gh-comment-id:4127727120 --> @madaxen86 commented on GitHub (Mar 25, 2026): > Any updates on this? Just went a removed nextCookies plugin for the time being. Caused me some headache today, too. Thanks for the great payload-auth package. Sidenote: better-auth@1.4 works with nextCookies
Author
Owner

@jellologic commented on GitHub (Mar 25, 2026):

The root cause of this infinite loop is that the nextCookies() after hook calls cookies().set() even when the cookie value hasn't changed. In Next.js Server Actions, cookies().set() triggers router cache invalidation regardless of whether the value actually changed.

Proposed fix for src/integrations/next-js.ts:

  parsed.forEach((value, key) => {
    if (!key) return;
+   // Skip setting cookies if the value hasn't changed.
+   // In Next.js Server Actions, cookies().set() triggers
+   // router cache invalidation even when the value is identical,
+   // causing infinite re-render loops (e.g. Payload CMS buildFormState).
+   try {
+     const existing = cookieHelper.get(key);
+     if (existing && existing.value === value.value) return;
+   } catch {}
    const opts = {
      sameSite: value.samesite,
      // ...
    };
    try {
      cookieHelper.set(key, value.value, opts);
    } catch {}
  });

Why this works

When getSession() is called from within a Server Action (like Payload CMS's buildFormState), the session cookie in the response is typically identical to what's already in the cookie store. By comparing before setting, we avoid the unnecessary cookies().set() call that triggers Next.js router cache invalidation.

Context

This fix has been verified working with Payload CMS 3.75.0 + payload-auth 1.9.1 + better-auth 1.5.6 + Next.js 16.1.6. The infinite buildFormState loop, field value reverting, and associated TypeError crash are all resolved.

Related: payload-auth/payload-auth#139

<!-- gh-comment-id:4129813395 --> @jellologic commented on GitHub (Mar 25, 2026): ## Fix: Compare cookie values before calling cookies().set() The root cause of this infinite loop is that the `nextCookies()` after hook calls `cookies().set()` even when the cookie value hasn't changed. In Next.js Server Actions, `cookies().set()` triggers router cache invalidation regardless of whether the value actually changed. ### Proposed fix for `src/integrations/next-js.ts`: ```diff parsed.forEach((value, key) => { if (!key) return; + // Skip setting cookies if the value hasn't changed. + // In Next.js Server Actions, cookies().set() triggers + // router cache invalidation even when the value is identical, + // causing infinite re-render loops (e.g. Payload CMS buildFormState). + try { + const existing = cookieHelper.get(key); + if (existing && existing.value === value.value) return; + } catch {} const opts = { sameSite: value.samesite, // ... }; try { cookieHelper.set(key, value.value, opts); } catch {} }); ``` ### Why this works When `getSession()` is called from within a Server Action (like Payload CMS's `buildFormState`), the session cookie in the response is typically identical to what's already in the cookie store. By comparing before setting, we avoid the unnecessary `cookies().set()` call that triggers Next.js router cache invalidation. ### Context This fix has been verified working with Payload CMS 3.75.0 + payload-auth 1.9.1 + better-auth 1.5.6 + Next.js 16.1.6. The infinite `buildFormState` loop, field value reverting, and associated `TypeError` crash are all resolved. Related: payload-auth/payload-auth#139
Author
Owner

@jellologic commented on GitHub (Mar 25, 2026):

After further testing, the cookie comparison fix I suggested earlier helps but does not fully resolve the issue. The session cookie values can legitimately differ between requests (e.g., updated expiry timestamps), so the comparison doesn't always prevent redundant cookies().set() calls.

The only reliable fix: Remove nextCookies() entirely

- import { nextCookies } from 'better-auth/next-js'

  plugins: [
-   nextCookies(),
  ]

Session cookies continue to work fine — better-auth manages them via response headers. The nextCookies() plugin is only needed if you want to proactively sync cookies into Next.js's cookie store (e.g., for middleware access), but the cost is breaking any framework that uses Server Actions for form state management (like Payload CMS).

Suggested long-term fix for better-auth

The root issue is that cookies().set() in a Next.js Server Action triggers router cache invalidation as a side effect, even when the value hasn't meaningfully changed. A proper fix would be for the nextCookies() after hook to detect when it's running inside a Server Action and skip cookie syncing, since Server Actions shouldn't be causing navigation-level cache invalidation. The current _flag === "router" check doesn't cover this case.

<!-- gh-comment-id:4130076571 --> @jellologic commented on GitHub (Mar 25, 2026): ## Update: Cookie comparison alone is not sufficient After further testing, the cookie comparison fix I suggested earlier helps but **does not fully resolve the issue**. The session cookie values can legitimately differ between requests (e.g., updated expiry timestamps), so the comparison doesn't always prevent redundant `cookies().set()` calls. ### The only reliable fix: Remove `nextCookies()` entirely ```diff - import { nextCookies } from 'better-auth/next-js' plugins: [ - nextCookies(), ] ``` Session cookies continue to work fine — better-auth manages them via response headers. The `nextCookies()` plugin is only needed if you want to proactively sync cookies into Next.js's cookie store (e.g., for middleware access), but the cost is breaking any framework that uses Server Actions for form state management (like Payload CMS). ### Suggested long-term fix for better-auth The root issue is that `cookies().set()` in a Next.js Server Action triggers router cache invalidation as a side effect, even when the value hasn't meaningfully changed. A proper fix would be for the `nextCookies()` after hook to **detect when it's running inside a Server Action** and skip cookie syncing, since Server Actions shouldn't be causing navigation-level cache invalidation. The current `_flag === "router"` check doesn't cover this case.
Author
Owner

@dwightware4 commented on GitHub (Apr 3, 2026):

This issue is caused by the before hook in nextCookies in src/integrations/next-js.ts.

// Detect Server Component by testing if cookies can be modified.
// In Server Components, `cookies().set()` throws an error.
// In Server Actions or Route Handlers, it succeeds.

...

try {
	cookieStore.set("__better-auth-cookie-store", "1", { maxAge: 0 });
	// If cookie was set successfully, we should clean up.
	cookieStore.delete("__better-auth-cookie-store");
} catch {

NextJS triggers a full page refresh anytime it sees a Set-Cookie header, and cookieStore.delete actually just sets the cookie with an expiration date in the past.

The breaking change was introduced 2 months ago in this PR: https://github.com/better-auth/better-auth/pull/7763, which changed the logic for detecting RSC context.

<!-- gh-comment-id:4181347785 --> @dwightware4 commented on GitHub (Apr 3, 2026): This issue is caused by the `before` hook in `nextCookies` in [src/integrations/next-js.ts](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/integrations/next-js.ts). ``` // Detect Server Component by testing if cookies can be modified. // In Server Components, `cookies().set()` throws an error. // In Server Actions or Route Handlers, it succeeds. ... try { cookieStore.set("__better-auth-cookie-store", "1", { maxAge: 0 }); // If cookie was set successfully, we should clean up. cookieStore.delete("__better-auth-cookie-store"); } catch { ``` NextJS triggers a full page refresh anytime it sees a `Set-Cookie` header, and `cookieStore.delete` actually just sets the cookie with an expiration date in the past. The breaking change was introduced 2 months ago in this PR: https://github.com/better-auth/better-auth/pull/7763, which changed the logic for detecting RSC context.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11097