From 9a229ce132a0e7a66ef2e2e5253ee1a2b2f0a79e Mon Sep 17 00:00:00 2001 From: Taesu <166604494+bytaesu@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:24:16 +0900 Subject: [PATCH] fix(client): preserve stale session data on network or server errors (#8437) --- packages/better-auth/src/client/query.test.ts | 141 ++++++++++++++++++ packages/better-auth/src/client/query.ts | 7 +- 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 packages/better-auth/src/client/query.test.ts diff --git a/packages/better-auth/src/client/query.test.ts b/packages/better-auth/src/client/query.test.ts new file mode 100644 index 0000000000..4ef796913f --- /dev/null +++ b/packages/better-auth/src/client/query.test.ts @@ -0,0 +1,141 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createAuthClient } from "./solid"; +import { testClientPlugin } from "./test-plugin"; + +/** + * @see https://github.com/better-auth/better-auth/issues/8420 + */ +describe("useAuthQuery - error handling", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("should preserve stale data on network error (fetch throws)", async () => { + let shouldFail = false; + + const client = createAuthClient({ + plugins: [testClientPlugin()], + fetchOptions: { + customFetchImpl: async (_url) => { + if (shouldFail) { + throw new TypeError("Failed to fetch"); + } + return new Response( + JSON.stringify({ + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + }), + ); + }, + baseURL: "http://localhost:3000", + }, + }); + + const session = client.useSession(); + await vi.runAllTimersAsync(); + + expect(session().data).toMatchObject({ + user: { id: "1", email: "test@test.com" }, + }); + expect(session().error).toBeNull(); + + // Network failure on refetch + shouldFail = true; + await session().refetch(); + await vi.runAllTimersAsync(); + + // Stale data should be preserved + expect(session().data).toMatchObject({ + user: { id: "1", email: "test@test.com" }, + }); + }); + + it("should clear data on 401 unauthorized response", async () => { + let returnUnauthorized = false; + + const client = createAuthClient({ + plugins: [testClientPlugin()], + fetchOptions: { + customFetchImpl: async (url) => { + const urlStr = typeof url === "string" ? url : url.toString(); + if (returnUnauthorized && urlStr.includes("/get-session")) { + return new Response(JSON.stringify({ message: "Unauthorized" }), { + status: 401, + }); + } + return new Response( + JSON.stringify({ + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + }), + ); + }, + baseURL: "http://localhost:3000", + }, + }); + + const session = client.useSession(); + await vi.runAllTimersAsync(); + + expect(session().data).toMatchObject({ + user: { id: "1", email: "test@test.com" }, + }); + + // Refetch with 401 + returnUnauthorized = true; + await session().refetch(); + await vi.runAllTimersAsync(); + + expect(session().data).toBeNull(); + }); + + it("should preserve stale data on 500 server error", async () => { + let returnServerError = false; + + const client = createAuthClient({ + plugins: [testClientPlugin()], + fetchOptions: { + customFetchImpl: async (url) => { + const urlStr = typeof url === "string" ? url : url.toString(); + if (returnServerError && urlStr.includes("/get-session")) { + return new Response( + JSON.stringify({ message: "Internal Server Error" }), + { status: 500 }, + ); + } + return new Response( + JSON.stringify({ + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + }), + ); + }, + baseURL: "http://localhost:3000", + }, + }); + + const session = client.useSession(); + await vi.runAllTimersAsync(); + + expect(session().data).toMatchObject({ + user: { id: "1", email: "test@test.com" }, + }); + + // Refetch with 500 + returnServerError = true; + await session().refetch(); + await vi.runAllTimersAsync(); + + // Stale data should be preserved on 500 + expect(session().data).toMatchObject({ + user: { id: "1", email: "test@test.com" }, + }); + }); +}); diff --git a/packages/better-auth/src/client/query.ts b/packages/better-auth/src/client/query.ts index e486960895..2271cfa1c8 100644 --- a/packages/better-auth/src/client/query.ts +++ b/packages/better-auth/src/client/query.ts @@ -79,9 +79,12 @@ export const useAuthQuery = ( : request.retry?.attempts; const retryAttempt = request.retryAttempt || 0; if (retryAttempts && retryAttempt < retryAttempts) return; + const isUnauthorized = context.error.status === 401; value.set({ error: context.error, - data: null, + data: isUnauthorized + ? null // clear session on HTTP 401 + : value.get().data, // preserve stale data on other errors isPending: false, isRefetching: false, refetch: value.value.refetch, @@ -103,7 +106,7 @@ export const useAuthQuery = ( .catch((error) => { value.set({ error, - data: null, + data: value.get().data, isPending: false, isRefetching: false, refetch: value.value.refetch,