diff --git a/.changeset/clean-rice-skip.md b/.changeset/clean-rice-skip.md
new file mode 100644
index 0000000000..fb0fbef6ef
--- /dev/null
+++ b/.changeset/clean-rice-skip.md
@@ -0,0 +1,5 @@
+---
+"better-auth": patch
+---
+
+Prevent `getSession` from failing when cookie caching is disabled while clients still have cached session cookies.
diff --git a/packages/better-auth/src/api/routes/cookie-cache-fallback.test.ts b/packages/better-auth/src/api/routes/cookie-cache-fallback.test.ts
index 4d76c834dc..2f2af62f42 100644
--- a/packages/better-auth/src/api/routes/cookie-cache-fallback.test.ts
+++ b/packages/better-auth/src/api/routes/cookie-cache-fallback.test.ts
@@ -1,7 +1,141 @@
import { afterEach, describe, expect, it, vi } from "vitest";
-import { parseCookies } from "../../cookies";
+import { parseCookies, parseSetCookieHeader } from "../../cookies";
+import { symmetricEncodeJWT } from "../../crypto";
import { getTestInstance } from "../../test-utils/test-instance";
+/**
+ * @see https://github.com/better-auth/better-auth/issues/11119
+ */
+describe("disabled cookie cache fallback", () => {
+ const secret = "better-auth-retired-cookie-cache-test-secret";
+ const cacheMaxAge = 5 * 60;
+
+ it.each([
+ {
+ mode: "in configuration",
+ cacheEnabled: false,
+ query: undefined,
+ expectedSessionDataMaxAge: 0,
+ },
+ {
+ mode: "for the request",
+ cacheEnabled: true,
+ query: { disableCookieCache: true },
+ expectedSessionDataMaxAge: cacheMaxAge,
+ },
+ ])("ignores retired session_data when caching is disabled $mode", async ({
+ cacheEnabled,
+ query,
+ expectedSessionDataMaxAge,
+ }) => {
+ const { client, testUser, cookieSetter } = await getTestInstance({
+ secret,
+ advanced: {
+ cookies: {
+ session_token: { name: "custom.session_token" },
+ session_data: { name: "custom.session" },
+ },
+ },
+ session: {
+ cookieCache: {
+ enabled: cacheEnabled,
+ strategy: "compact",
+ maxAge: cacheMaxAge,
+ },
+ },
+ });
+ const headers = new Headers();
+ await client.signIn.email(
+ { email: testUser.email, password: testUser.password },
+ { onSuccess: cookieSetter(headers) },
+ );
+
+ const retiredCache = await symmetricEncodeJWT(
+ { retired: true },
+ secret,
+ "better-auth-session",
+ );
+ headers.set(
+ "cookie",
+ `${headers.get("cookie")}; custom.session=${retiredCache}; custom.session.0=stale; custom.session.1=chunks`,
+ );
+
+ let setCookieHeader = "";
+ const session = await client.getSession({
+ query,
+ fetchOptions: {
+ headers,
+ onSuccess(context) {
+ setCookieHeader = context.response.headers.get("set-cookie") || "";
+ },
+ },
+ });
+ expect(session.data?.user.email).toBe(testUser.email);
+
+ const responseCookies = parseSetCookieHeader(setCookieHeader);
+ expect(responseCookies.get("custom.session")?.["max-age"]).toBe(
+ expectedSessionDataMaxAge,
+ );
+ for (const name of ["custom.session.0", "custom.session.1"]) {
+ expect(responseCookies.get(name)?.["max-age"]).toBe(0);
+ }
+ expect(responseCookies.has("custom.session_token")).toBe(false);
+ });
+
+ it.each([
+ { state: "missing", sessionTokenCookie: "" },
+ {
+ state: "invalid",
+ sessionTokenCookie: "; custom.session_token=invalid",
+ },
+ ])("clears retired session_data when session_token is $state", async ({
+ sessionTokenCookie,
+ }) => {
+ const { client } = await getTestInstance({
+ secret,
+ advanced: {
+ cookies: {
+ session_token: { name: "custom.session_token" },
+ session_data: { name: "custom.session" },
+ },
+ },
+ session: {
+ cookieCache: { enabled: false },
+ },
+ });
+ const retiredCache = await symmetricEncodeJWT(
+ { retired: true },
+ secret,
+ "better-auth-session",
+ );
+ const headers = new Headers({
+ cookie:
+ `custom.session=${retiredCache}; custom.session.0=stale; custom.session.1=chunks` +
+ sessionTokenCookie,
+ });
+
+ let setCookieHeader = "";
+ const session = await client.getSession({
+ fetchOptions: {
+ headers,
+ onSuccess(context) {
+ setCookieHeader = context.response.headers.get("set-cookie") || "";
+ },
+ },
+ });
+
+ expect(session.data).toBeNull();
+ const responseCookies = parseSetCookieHeader(setCookieHeader);
+ for (const name of [
+ "custom.session",
+ "custom.session.0",
+ "custom.session.1",
+ ]) {
+ expect(responseCookies.get(name)?.["max-age"]).toBe(0);
+ }
+ });
+});
+
/**
* @see https://github.com/better-auth/better-auth/issues/9233
*
@@ -19,6 +153,7 @@ describe("cookieCache HMAC verification failure fallback", async () => {
afterEach(() => {
vi.useRealTimers();
});
+
it("should fall through to session_token DB validation when session_data HMAC fails", async () => {
const { client, testUser, cookieSetter } = await getTestInstance({
session: {
diff --git a/packages/better-auth/src/api/routes/session.ts b/packages/better-auth/src/api/routes/session.ts
index 2380d4ccc6..a8e3d091df 100644
--- a/packages/better-auth/src/api/routes/session.ts
+++ b/packages/better-auth/src/api/routes/session.ts
@@ -11,6 +11,7 @@ import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import * as z from "zod";
import { hasServerSessionStore } from "../../context/store-capabilities";
import {
+ createSessionStore,
decodeCookieCache,
deleteSessionCookie,
expireCookie,
@@ -83,12 +84,14 @@ export const getSession =