Bug report
Package: @supabase/auth-js (via @supabase/supabase-js)
Version observed: 2.110.0
File: packages/core/auth-js/src/GoTrueClient.ts, method _signOut
What happens
GoTraClient._signOut({ scope }) always calls the network first:
async _signOut({ scope } = { scope: 'global' }) {
return await this._useSession(async (result) => {
...
const accessToken = data.session?.access_token;
if (accessToken) {
const { error } = await this.admin.signOut(accessToken, scope); // <-- network call
if (error) {
if (!(/* 404/401/403/session-missing */)) {
return this._returnResult({ error }); // returns, but at least reaches here
}
}
}
if (scope !== 'others') {
await this._removeSession(); // <-- local cleanup, only reached if the above didn't throw
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
}
return this._returnResult({ error: null });
});
}
If the client is offline, this.admin.signOut(accessToken, scope) performs a fetch that throws (network error), rather than resolving with { error }. That exception propagates out of _signOut() uncaught, so _removeSession() is never reached — the local session/token is never removed from storage, regardless of scope (including scope: 'local', which a caller would reasonably expect to never touch the network at all).
Impact
An app calling supabase.auth.signOut() (or { scope: 'local' }) while offline gets a rejected promise, but the user is still authenticated locally (valid session token still in storage) after the call — even though the app has already navigated away as if logged out. Reloading the page, or any code path that reads the persisted session, will find the user still "logged in" with no indication anything went wrong, unless the caller wraps every signOut() in a try/catch AND manually clears the storage key itself as a fallback (which defeats the purpose of having a public signOut() API).
Expected behavior
scope: 'local' should never perform a network call at all (it's documented as "sign out only on this device/browser"), or
- at minimum,
_removeSession() should run in a finally (or before attempting the network call, followed by a best-effort network revoke) so that local session state is guaranteed to be cleared even if the network request fails/throws.
Repro
// Simulate offline (e.g. via navigator.onLine / fetch mock)
const { error } = await supabase.auth.signOut(); // rejects (or resolves with error) while offline
// Session token is STILL present in the configured storage (e.g. localStorage[storageKey])
Workaround we're using
We manually remove the storage key(s) ourselves when signOut() throws or returns an error, as a forced fallback, before treating the local logout as complete. Happy to open a PR if a fix direction is agreed (e.g. moving _removeSession() into a finally, or making scope: 'local' skip the network call entirely).
Bug report
Package:
@supabase/auth-js(via@supabase/supabase-js)Version observed: 2.110.0
File:
packages/core/auth-js/src/GoTrueClient.ts, method_signOutWhat happens
GoTraClient._signOut({ scope })always calls the network first:If the client is offline,
this.admin.signOut(accessToken, scope)performs afetchthat throws (network error), rather than resolving with{ error }. That exception propagates out of_signOut()uncaught, so_removeSession()is never reached — the local session/token is never removed from storage, regardless ofscope(includingscope: 'local', which a caller would reasonably expect to never touch the network at all).Impact
An app calling
supabase.auth.signOut()(or{ scope: 'local' }) while offline gets a rejected promise, but the user is still authenticated locally (valid session token still in storage) after the call — even though the app has already navigated away as if logged out. Reloading the page, or any code path that reads the persisted session, will find the user still "logged in" with no indication anything went wrong, unless the caller wraps everysignOut()in a try/catch AND manually clears the storage key itself as a fallback (which defeats the purpose of having a publicsignOut()API).Expected behavior
scope: 'local'should never perform a network call at all (it's documented as "sign out only on this device/browser"), or_removeSession()should run in afinally(or before attempting the network call, followed by a best-effort network revoke) so that local session state is guaranteed to be cleared even if the network request fails/throws.Repro
Workaround we're using
We manually remove the storage key(s) ourselves when
signOut()throws or returns an error, as a forced fallback, before treating the local logout as complete. Happy to open a PR if a fix direction is agreed (e.g. moving_removeSession()into afinally, or makingscope: 'local'skip the network call entirely).