-
Notifications
You must be signed in to change notification settings - Fork 578
Description
Describe the bug
Supabase Auth-JS Deadlock Issue
Summary
supabase.auth.signInWithPassword() and supabase.auth.setSession() hang indefinitely after receiving a successful 200 OK response from the Supabase auth API. The deadlock occurs in the _acquireLock mechanism during session storage operations.
Environment
- Package:
@supabase/auth-js - Version: Latest (as of January 2026)
- Browser: Chrome/Edge (Chromium-based)
- Platform: Windows
- Framework: React + Vite
Problem Description
Observed Behavior
- Call
supabase.auth.signInWithPassword({ email, password }) - Network tab shows successful 200 OK response from
/auth/v1/token?grant_type=password - Response body contains valid session data (access_token, refresh_token, user)
- Function never resolves or rejects - hangs indefinitely
- Browser tab becomes unresponsive
- Same issue occurs with
supabase.auth.setSession()
Expected Behavior
- Function should resolve with session data after successful authentication
- Session should be saved to storage
- User should be signed in
Root Cause Analysis
Primary Issue: Deadlock in _acquireLock Mechanism
File: node_modules/@supabase/auth-js/src/GoTrueClient.ts
Line 1485-1549: _acquireLock implementation
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
// ...
return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
this.lockAcquired = true
const result = fn()
// ...
await result
// ...
})
}Line 672-713: signInWithPassword calls _acquireLock
async signInWithPassword(credentials: SignInWithPasswordCredentials): Promise<AuthResponse> {
// ...
return this._acquireLock(-1, async () => {
return await this._signInWithPassword(credentials)
})
}Line 1854-1856: setSession also calls _acquireLock
async setSession(currentSession: { access_token: string; refresh_token: string }): Promise<AuthResponse> {
await this.initializePromise
return await this._acquireLock(-1, async () => {
return await this._setSession(currentSession)
})
}Line 2769-2816: _saveSession attempts storage operations while lock is held
protected async _saveSession(session: Session) {
// ...
await this.storage.setItem(this.storageKey, JSON.stringify(session))
// This storage operation hangs when lock is already acquired
}Deadlock Chain
- Step 1:
signInWithPassword()→ acquires navigator.locks lock → calls_signInWithPassword() - Step 2:
_signInWithPassword()→ receives 200 OK from API → calls_saveSession() - Step 3:
_saveSession()→ attemptsstorage.setItem()while lock is held → HANGS - Lock never releases → Promise never resolves → Function hangs forever
Why the Lock Mechanism Fails
The _acquireLock method uses navigator.locks API (via this.lock()) which creates a mutual exclusion lock across tabs/windows. However:
- Reentrancy Issue: The lock is not properly reentrant - storage operations inside the lock can trigger additional lock-related events
- Storage Contention: Browser's storage API may try to acquire its own locks, conflicting with navigator.locks
- Event Loop Blocking: The lock mechanism may be blocking the event loop, preventing storage operations from completing
- Race Condition: Multiple async operations within the lock (
setItemAsync,deepClone) may create circular wait conditions
Steps to Reproduce
Minimal Reproduction
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key',
{
auth: {
autoRefreshToken: true,
persistSession: true,
storageKey: 'fashion_web_session',
storage: window.localStorage, // Or window.sessionStorage
},
}
);
// This will hang indefinitely
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'validpassword123',
});
console.log('This line never executes');Debug Output
Console logs show:
1. Before signInWithPassword call
2. Network: 200 OK from /auth/v1/token
3. Response contains: { access_token, refresh_token, user, ... }
4. [HANGS - no further logs]
Network tab shows:
POST /auth/v1/token?grant_type=password
Status: 200 OK
Response: Valid session JSON
Affected Methods
All methods using _acquireLock are potentially affected:
signInWithPassword()(Line 672)signUp()(Line 752)signInWithOAuth()(Line 1386)signInWithIdToken()(Line 1473)setSession()(Line 1854)⚠️ Cannot be used as workaroundrefreshSession()(Line 1927)getSession()(Line 1719)signOut()(Line 2141)- And 10+ other auth methods
Impact
Severity: CRITICAL
- Completely blocks user authentication
- No workaround using official SDK
- Affects all authentication methods
- Production applications cannot function
Scope
- All browser-based applications using @supabase/auth-js
- Both localStorage and sessionStorage configurations
- All Chromium-based browsers (Chrome, Edge, Brave, etc.)
Proposed Fix
Option 1: Remove Navigator Locks Dependency (Recommended)
Replace navigator.locks with a simpler in-memory lock mechanism that doesn't interact with browser APIs:
private lockQueue: Promise<any> = Promise.resolve();
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
const currentLock = this.lockQueue;
let releaseLock: () => void;
this.lockQueue = new Promise(resolve => {
releaseLock = resolve;
});
try {
await currentLock; // Wait for previous operation
return await fn();
} finally {
releaseLock!(); // Release for next operation
}
}Option 2: Make Storage Operations Non-Blocking
Move storage operations outside the lock acquisition:
protected async _saveSession(session: Session) {
const serialized = JSON.stringify(session);
// Release lock before storage operation
await Promise.resolve();
// Now safe to write to storage
await this.storage.setItem(this.storageKey, serialized);
}Option 3: Add Timeout and Recovery
Add timeout mechanism to prevent infinite hangs:
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
const timeoutMs = acquireTimeout === -1 ? 10000 : acquireTimeout; // Default 10s max
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Lock acquisition timeout')), timeoutMs)
);
return await Promise.race([
this.lock(`lock:${this.storageKey}`, acquireTimeout, fn),
timeout,
]);
}Workaround (Current Implementation)
Since the official SDK is broken, we've implemented a raw HTTP API approach:
Raw HTTP Sign-In
const response = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': supabaseAnonKey,
'Authorization': `Bearer ${supabaseAnonKey}`,
},
body: JSON.stringify({ email, password }),
});
const authData = await response.json();
// Manual session storage (bypass Supabase client completely)
sessionStorage.setItem('fashion_web_session', JSON.stringify({
access_token: authData.access_token,
refresh_token: authData.refresh_token,
expires_in: authData.expires_in,
expires_at: Math.floor(Date.now() / 1000) + authData.expires_in,
user: authData.user,
}));Note: Cannot use supabase.auth.setSession() as workaround - it uses the same broken _acquireLock mechanism.
Testing Recommendations
Test Cases
- ✅ Sign in with valid credentials
- ✅ Sign in with invalid credentials
- ✅ Sign up new user
- ✅ Refresh expired token
- ✅ Sign out
- ✅ Concurrent auth operations across multiple tabs
- ✅ Storage quota exceeded scenarios
- ✅ Network failure recovery
Lock Contention Tests
- Multiple tabs attempting sign-in simultaneously
- Rapid successive auth calls
- Background token refresh during user interaction
- Browser back/forward navigation during auth
Additional Context
Browser Console Warnings
None - the hang is silent with no errors logged
Storage Configuration
{
auth: {
autoRefreshToken: true,
persistSession: true,
storageKey: 'fashion_web_session',
storage: window.sessionStorage, // Issue occurs with both localStorage and sessionStorage
}
}Related Issues
- Similar to Web Locks API issues in other libraries
- May be related to storage event listeners in cross-tab synchronization
- Potentially related to async storage adapter implementations
References
- GoTrueClient Source:
@supabase/auth-js/src/GoTrueClient.ts - Navigator Locks API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/locks
- Supabase Auth API: https://supabase.com/docs/reference/javascript/auth-signinwithpassword
Reporter: Datrisoft
Date: January 10, 2026
Priority: P0 - Critical
Category: Bug - Deadlock/Hang
Library affected
supabase-js
Reproduction
No response
Steps to reproduce
No response
System Info
Package: @supabase/supabase-js` v2.89.0 (includes `@supabase/auth-js`)
- Node.js: v24.11.1
- npm: v11.6.2
- React: v19.2.0
- Vite: v7.2.4 (runtime v7.3.0)
- TypeScript: v5.9.3
- Browser: Chrome/Edge (Chromium-based)
- OS: Windows 10 Pro (Build 2009, 64-bit)
- Storage: Both "localStorage" and "sessionStorage" affectedUsed Package Manager
npm
Logs
No response
Validations
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Make sure this is a Supabase JS Library issue and not an issue with the Supabase platform. If it's a Supabase platform related bug, it should likely be reported to supabase/supabase instead.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Discord Chat Server.
- The provided reproduction is a minimal reproducible example of the bug.