diff --git a/packages/better-auth/src/client/session-refresh.test.ts b/packages/better-auth/src/client/session-refresh.test.ts index 8c78d0d880..bd844e8d1c 100644 --- a/packages/better-auth/src/client/session-refresh.test.ts +++ b/packages/better-auth/src/client/session-refresh.test.ts @@ -725,6 +725,103 @@ describe("session-refresh", () => { vi.useRealTimers(); }); + it("should preserve session when $fetch returns unwrapped data (throw: true)", async () => { + vi.useFakeTimers(); + + const initialData = { + user: { id: "1", email: "old@test.com" }, + session: { id: "session-1" }, + }; + + const refreshedData = { + user: { id: "1", email: "new@test.com" }, + session: { id: "session-1" }, + }; + + const sessionAtom: SessionAtom = atom({ + data: initialData, + error: null, + isPending: false, + isRefetching: false, + }); + const sessionSignal = atom(false); + + // When throw: true, $fetch returns data directly instead of { data, error } + const mockFetch = vi.fn(async () => refreshedData); + + const manager = createSessionRefreshManager({ + sessionAtom, + sessionSignal, + $fetch: mockFetch as any, + options: { + sessionOptions: { + refetchInterval: 5, + }, + }, + }); + + manager.init(); + + await vi.advanceTimersByTimeAsync(5000); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const updatedSession = sessionAtom.get(); + expect(updatedSession.data).toEqual(refreshedData); + expect(updatedSession.data?.user?.email).toBe("new@test.com"); + + manager.cleanup(); + vi.useRealTimers(); + }); + + it("should preserve session on visibilitychange when $fetch returns unwrapped data (throw: true)", async () => { + vi.useFakeTimers(); + + const initialData = { + user: { id: "1", email: "old@test.com" }, + session: { id: "session-1" }, + }; + + const refreshedData = { + user: { id: "1", email: "new@test.com" }, + session: { id: "session-1" }, + }; + + const sessionAtom: SessionAtom = atom({ + data: initialData, + error: null, + isPending: false, + isRefetching: false, + }); + const sessionSignal = atom(false); + + // When throw: true, $fetch returns data directly + const mockFetch = vi.fn(async () => refreshedData); + + const manager = createSessionRefreshManager({ + sessionAtom, + sessionSignal, + $fetch: mockFetch as any, + options: { + sessionOptions: { + refetchOnWindowFocus: true, + }, + }, + }); + + manager.init(); + + manager.triggerRefetch({ event: "visibilitychange" }); + await vi.runAllTimersAsync(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const updatedSession = sessionAtom.get(); + expect(updatedSession.data).toEqual(refreshedData); + expect(updatedSession.data?.user?.email).toBe("new@test.com"); + + manager.cleanup(); + vi.useRealTimers(); + }); + it("should broadcast session update when broadcastSessionUpdate is called with signout", () => { const channel = getGlobalBroadcastChannel(); const postSpy = vi.spyOn(channel, "post"); diff --git a/packages/better-auth/src/client/session-refresh.ts b/packages/better-auth/src/client/session-refresh.ts index a4c117c6ff..a7681806bd 100644 --- a/packages/better-auth/src/client/session-refresh.ts +++ b/packages/better-auth/src/client/session-refresh.ts @@ -9,6 +9,24 @@ import type { AuthQueryAtom } from "./query"; const now = () => Math.floor(Date.now() / 1000); +/** + * Normalize $fetch response: `throw: true` returns data directly, otherwise `{ data, error }`. + */ +function normalizeSessionResponse(res: unknown): { + data: SessionResponse | null; + error: unknown; +} { + if ( + typeof res === "object" && + res !== null && + "data" in res && + "error" in res + ) { + return res as { data: SessionResponse | null; error: unknown }; + } + return { data: res as SessionResponse, error: null }; +} + /** * Rate limit: don't refetch on focus if a session request was made within this many seconds */ @@ -90,16 +108,14 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) { state.lastSessionRequest = now(); $fetch("/get-session") .then(async (res) => { - let data = res.data; - let error = res.error || null; + let { data, error } = normalizeSessionResponse(res); if (data?.needsRefresh) { try { const refreshRes = await $fetch("/get-session", { method: "POST", }); - data = refreshRes.data; - error = refreshRes.error || null; + ({ data, error } = normalizeSessionResponse(refreshRes)); } catch {} }