fix(client): preserve stale session data on network or server errors (#8437)

This commit is contained in:
Taesu
2026-03-06 10:11:06 -08:00
committed by Alex Yang
parent d364eff684
commit 9a229ce132
2 changed files with 146 additions and 2 deletions
@@ -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" },
});
});
});
+5 -2
View File
@@ -79,9 +79,12 @@ export const useAuthQuery = <T>(
: 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 = <T>(
.catch((error) => {
value.set({
error,
data: null,
data: value.get().data,
isPending: false,
isRefetching: false,
refetch: value.value.refetch,