Skip to content

fix(auth): recover from orphaned navigator locks via steal fallback#2106

Merged
mandarini merged 2 commits intosupabase:masterfrom
ElliotPadfield:fix/navigator-lock-orphan-recovery
Feb 19, 2026
Merged

fix(auth): recover from orphaned navigator locks via steal fallback#2106
mandarini merged 2 commits intosupabase:masterfrom
ElliotPadfield:fix/navigator-lock-orphan-recovery

Conversation

@ElliotPadfield
Copy link
Contributor

@ElliotPadfield ElliotPadfield commented Feb 8, 2026

Summary

Fixes #2111

When a Navigator Lock is held indefinitely (e.g., due to React Strict Mode's double-mount/unmount leaving an orphaned lock callback in GoTrueClient._acquireLock's pendingInLock drain loop), all subsequent auth operations (getUser(), signInWithPassword(), etc.) hang forever.

Root cause: The existing acquireTimeout mechanism uses AbortController.abort() to cancel pending lock requests. Per the Web Locks API spec, aborting the signal only removes pending (waiting-to-acquire) requests from the queue — it has no effect on an already-held lock. So when a lock is orphaned:

  1. The held lock callback never returns → lock stays held forever
  2. Subsequent callers queue up, timeout, get AbortError, and give up
  3. The auth system is permanently broken until all tabs are closed

Fix: When lock acquisition times out with AbortError, instead of propagating the error, retry with { steal: true }. Per the spec, this releases any currently held lock with the same name and grants the request immediately. The previous holder's callback continues running to completion but no longer blocks other callers.

Changes

  • packages/core/auth-js/src/lib/locks.ts — Wrap navigator.locks.request() in try/catch; on AbortError with positive acquireTimeout, retry with { steal: true } to recover from orphaned locks
  • packages/core/auth-js/test/lib/locks.test.ts — Add 3 tests covering: orphaned lock recovery via steal, non-AbortError passthrough, no steal attempt with negative timeout

Test plan

  • All 10 existing + new unit tests pass (jest --testPathPattern='test/lib/locks')
  • Manual verification: create a Next.js app with React Strict Mode, call getUser() in a useEffect, trigger the orphaned lock scenario (visible via navigator.locks.query()), confirm auth recovers instead of hanging
  • Verify no regression in multi-tab session synchronization

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved lock acquisition reliability with enhanced timeout handling and automatic recovery from orphaned locks.
    • Enhanced error messages and logging for timeout scenarios to clarify potential causes and recovery actions.
  • Tests

    • Added test coverage for lock recovery from timeouts, error propagation, and timeout configuration edge cases.

@ElliotPadfield ElliotPadfield requested review from a team as code owners February 8, 2026 03:29
@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

Rewrites the navigatorLock flow to introduce timeout-based lock acquisition with automatic recovery via lock stealing. When lock acquisition times out, the system attempts to force-acquire the lock with the steal: true option and executes the provided function, while adding detailed logging and error handling throughout.

Changes

Cohort / File(s) Summary
Lock acquisition with timeout recovery
packages/core/auth-js/src/lib/locks.ts
Refactors navigatorLock to wrap lock requests in try/catch, introduces AbortController-based timeout handling, adds recovery path that steals orphaned locks on acquire timeout, and logs acquisition, timeout, theft, and release events. Preserves immediate failure (timeout: 0) behavior with enhanced messaging.
Lock recovery test coverage
packages/core/auth-js/test/lib/locks.test.ts
Adds three test cases: lock recovery by stealing after acquire timeout, error propagation for non-AbortError exceptions, and verification that negative timeouts skip steal logic.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant NL as navigator.locks
    participant AC as AbortController
    participant Recovery as Recovery Logic
    participant Fn as Provided Function

    Client->>NL: request(lock, {signal})
    AC->>AC: timeout fires
    AC-->>NL: abort signal
    NL-->>Client: AbortError
    Client->>Recovery: acquireTimeout > 0?
    
    alt Timeout occurred and acquireTimeout > 0
        Recovery->>NL: request(lock, {steal: true})
        NL-->>Recovery: lock acquired
        Recovery->>Fn: execute()
        Fn-->>Recovery: result
        Recovery->>NL: release lock
    else Timeout on immediate acquire (acquireTimeout === 0)
        Recovery-->>Client: throw NavigatorLockAcquireTimeoutError
    else Non-AbortError exception
        Recovery-->>Client: propagate error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: fixing auth operations that hang due to orphaned navigator locks by implementing a steal fallback recovery mechanism.
Linked Issues check ✅ Passed The PR directly addresses all primary coding objectives from issue #42505: implements timeout-based recovery with steal fallback to prevent indefinite hangs, handles React Strict Mode orphaned locks, and preserves multi-tab session synchronization.
Out of Scope Changes check ✅ Passed All changes are scoped to navigator lock recovery: modifications to locks.ts implement the steal-fallback mechanism, test additions verify recovery behavior, and no unrelated alterations are introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@ElliotPadfield
Copy link
Contributor Author

Once I'd diagnosed & planned a fix, used Claude Code to help validate & polish. Happy to take any feedback & tweak/resubmit. Passes formatting, tests, build. Thanks to @coppinger for troubleshooting support as well.

@coppinger
Copy link

+1 that this is causing major headaches

@mandarini mandarini self-assigned this Feb 11, 2026
@mandarini
Copy link
Contributor

Thank you for this PR @ElliotPadfield , it has sparked an interesting (and educative for me) conversation within the team. As I mentioned here, before considering using the steal solution, we would like to see a reproduction repository where we can verify the orphaning. Would this be possible for you to share with me? It would be indeed super helpful!

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 11, 2026

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2106

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2106

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2106

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2106

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2106

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2106

commit: f931f90

@ElliotPadfield
Copy link
Contributor Author

Thank you for this PR @ElliotPadfield , it has sparked an interesting (and educative for me) conversation within the team. As I mentioned here, before considering using the steal solution, we would like to see a reproduction repository where we can verify the orphaning. Would this be possible for you to share with me? It would be indeed super helpful!

Will throw something together & coordinate with @coppinger. Thanks for your attention to the issue.

@mandarini
Copy link
Contributor

@ElliotPadfield

After lots of internal discussions, we have agreed to approve this PR, until we come up with a solution to the core of the problem, which may bring wider architectural changes.

To approve this PR, can you please resolve the conflicts with master?

After this PR is merged, we also agreed to decrease the timeout to 5s instead of 10s, but this is out of scope for this PR, since the change was introduced elsewhere.

@andreas-myklebust
Copy link

@mandarini Any way you could expedite the process by resolving said conflicts? Been tracking this issue for over a week now, would appreciate it greatly to have it resolved.

ElliotPadfield and others added 2 commits February 19, 2026 16:10
When a Navigator Lock is held indefinitely (e.g., due to React Strict
Mode's double-mount/unmount leaving an orphaned lock callback), all
subsequent auth operations hang forever because:

1. The acquireTimeout fires and aborts the pending lock request
2. The AbortError propagates, but the orphaned held lock is unaffected
3. All future callers timeout and fail with the same AbortError

This adds a recovery mechanism: when lock acquisition times out with
AbortError (indicating a likely orphaned lock), retry with
{ steal: true } to forcefully acquire the lock per the Web Locks API
spec. The previous holder's callback continues to completion but no
longer blocks other callers.

Closes supabase/supabase#42505

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@mandarini mandarini force-pushed the fix/navigator-lock-orphan-recovery branch from 03f258d to f931f90 Compare February 19, 2026 14:25
@mandarini mandarini merged commit b791178 into supabase:master Feb 19, 2026
16 checks passed
@mandarini mandarini linked an issue Feb 19, 2026 that may be closed by this pull request
7 tasks
mandarini added a commit to supabase/supabase that referenced this pull request Feb 25, 2026
The SDK now handles orphaned lock recovery via steal internally
(supabase-js#2106). Keep the BroadcastChannel observability wrapper for
Sentry signals. The steal-based orphaned lock recovery in
`debuggableNavigatorLock` (packages/common/gotrue.ts) (introduced in
#39868) is now redundant,
supabase-js#2106 handles this natively in the SDK.

Removes the `navigator.locks.request({ steal: true })` block while
keeping the BroadcastChannel wrapper that sends lock-holder stack traces
to Sentry.

Related: supabase/supabase-js#2106, supabase/supabase-js#2125
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants