Skip to content

feat(auth): add maxAutoRefreshFailures option - #2573

Open
7vignesh wants to merge 1 commit into
supabase:masterfrom
7vignesh:feat/auth-max-refresh-failures
Open

feat(auth): add maxAutoRefreshFailures option#2573
7vignesh wants to merge 1 commit into
supabase:masterfrom
7vignesh:feat/auth-max-refresh-failures

Conversation

@7vignesh

Copy link
Copy Markdown

Description

What changed?

Added an opt-in maxAutoRefreshFailures option to the auth client that stops the auto-refresh ticker after N consecutive failed ticks and emits a new TOKEN_REFRESH_FAILED event via onAuthStateChange.

Why was this change needed?

When the auth server is permanently unreachable (wrong URL, server down, DNS failure), the auto-refresh ticker retries every 30 seconds forever with no way for the app to know it's failing. Users reported pages hanging for 14-21 seconds on load due to retry backoff, with no signal to react to.

This was raised in #1680 and acknowledged by @mandarini as a valid feature request for a configurable "stop after N failed ticks and emit an error event" option that is opt-in.

How it works

  • New option maxAutoRefreshFailures (defaults to 0 which means retry forever, preserving existing behavior)
  • A counter tracks consecutive failed auto-refresh ticks
  • Counter resets to 0 on any successful token refresh
  • When the counter reaches the configured limit, the client stops the auto-refresh ticker and emits TOKEN_REFRESH_FAILED through onAuthStateChange

Usage

const supabase = createClient(URL, KEY, {
  auth: { maxAutoRefreshFailures: 5 }
})

supabase.auth.onAuthStateChange((event) => {
  if (event === 'TOKEN_REFRESH_FAILED') {
    // Server unreachable after 5 consecutive failures (~2.5 min)
    // Redirect to login or show a banner
  }
})

Closes #1680

Breaking changes

  • This PR contains no breaking changes

The option defaults to 0 (no limit), so existing behavior is completely unchanged. The new TOKEN_REFRESH_FAILED event is additive and only fires when the option is explicitly configured.

Checklist

Testing

  • 5 new unit tests covering: indefinite retry (default), stop after max failures, counter reset on success, no false positive events, thrown error handling
  • All 170 existing auth-js tests pass
  • Build passes for both auth-js and supabase-js

Additional notes

  • Both the legacy lock path and the default lockless path are covered
  • The failure counter is also reset inside _callRefreshToken on success, so any refresh path (manual or automatic) resets it
  • Zero runtime impact when the option is not set (the counter increments but the check short-circuits on maxAutoRefreshFailures === 0)

@7vignesh
7vignesh requested a review from a team as a code owner July 28, 2026 22:22
Copilot AI review requested due to automatic review settings July 28, 2026 22:22
@7vignesh
7vignesh requested a review from a team as a code owner July 28, 2026 22:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 573953cb-fbf1-4047-b553-ca9a4a9aeebd

📥 Commits

Reviewing files that changed from the base of the PR and between 07b27ee and 823388f.

📒 Files selected for processing (6)
  • packages/core/auth-js/src/GoTrueClient.ts
  • packages/core/auth-js/src/lib/types.ts
  • packages/core/auth-js/test/GoTrueClient.autoRefreshFailures.test.ts
  • packages/core/supabase-js/src/SupabaseClient.ts
  • packages/core/supabase-js/src/lib/types.ts
  • packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts
  • packages/core/supabase-js/src/lib/types.ts
  • packages/core/auth-js/src/lib/types.ts
  • packages/core/supabase-js/src/SupabaseClient.ts
  • packages/core/auth-js/test/GoTrueClient.autoRefreshFailures.test.ts
  • packages/core/auth-js/src/GoTrueClient.ts

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added maxAutoRefreshFailures to limit consecutive automatic token refresh failures; 0 keeps retries unlimited.
    • Automatic refresh now stops and emits TOKEN_REFRESH_FAILED when the configured limit is reached.
    • The option is available through Supabase Auth configuration.
  • Bug Fixes
    • Improved failure tracking and counter resets after successful authentication or session removal.
  • Tests
    • Added coverage for retry limits, resets, event emissions, and error handling.

Walkthrough

GoTrueClient adds configurable handling for consecutive auto-refresh failures. A value of 0 keeps retries unlimited. Retryable session-loading errors, refresh failures, and tick exceptions are counted in lock-based and lockless paths. Successful session saves and session removal reset the counter. Reaching the configured limit stops auto-refresh and emits TOKEN_REFRESH_FAILED. SupabaseClient forwards the option, with tests covering the behavior.

Sequence Diagram(s)

sequenceDiagram
  participant AutoRefreshTicker
  participant GoTrueClient
  participant RefreshToken
  participant AuthStateListener
  AutoRefreshTicker->>GoTrueClient: Run auto-refresh tick
  GoTrueClient->>RefreshToken: Attempt token refresh
  RefreshToken-->>GoTrueClient: Return success or retryable failure
  GoTrueClient->>GoTrueClient: Update consecutive failure count
  GoTrueClient->>AuthStateListener: Emit TOKEN_REFRESH_FAILED at threshold
  GoTrueClient->>AutoRefreshTicker: Stop auto-refresh ticker
Loading

Assessment against linked issues

Objective Addressed Explanation
Stop token refresh attempts after a configurable number of failures [#1680]

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/auth-js/src/GoTrueClient.ts`:
- Around line 5582-5592: Count refresh errors returned by _useSession() in both
auto-refresh tick paths: update GoTrueClient.ts lines 5582-5592 and 5510-5520 to
apply the existing auto-refresh failure handling, including incrementing
autoRefreshFailureCount and invoking _handleAutoRefreshFailure(), when the
session result contains an error. Extend
GoTrueClient.autoRefreshFailures.test.ts lines 54-58 to exercise the real
_useSession()/__loadSession() refresh path and verify these failures are
counted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c64895b-6ddf-49c0-ab95-e3a1520eb507

📥 Commits

Reviewing files that changed from the base of the PR and between a262492 and b1eeaae.

📒 Files selected for processing (3)
  • packages/core/auth-js/src/GoTrueClient.ts
  • packages/core/auth-js/src/lib/types.ts
  • packages/core/auth-js/test/GoTrueClient.autoRefreshFailures.test.ts

Comment thread packages/core/auth-js/src/GoTrueClient.ts
@7vignesh
7vignesh force-pushed the feat/auth-max-refresh-failures branch from b1eeaae to f64ad7d Compare July 28, 2026 22:32
@7vignesh

Copy link
Copy Markdown
Author

@mandarini pls review

@mandarini mandarini left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @7vignesh, thank you so much for contributing to Supabase! This is a well-shaped implementation of exactly the opt-in behavior we discussed in #1680! There are a few things that need to change before we can merge it.

  1. The option never reaches auth-js when set through createClient. SupabaseClientOptions['auth'] is an explicit field list (packages/core/supabase-js/src/lib/types.ts) and _initSupabaseAuthClient destructures an allow-list (packages/core/supabase-js/src/SupabaseClient.ts), so the usage example in the PR description is a type error for TypeScript users and a silent no-op for JavaScript users. Please add maxAutoRefreshFailures to both, plus a small supabase-js test that reads the option off the client to verify flow-through (we do the same for lockAcquireTimeout).

  2. The counter goes stale because it only resets on a successful refresh. If a user hits the limit and later signs back in, the count is still at max, so one transient blip will stop auto-refresh and emit TOKEN_REFRESH_FAILED after a single failure instead of N. Related: _onVisibilityChanged restarts the ticker on every tab focus, so during an outage the event re-fires on each refocus. Resetting the counter in _startAutoRefresh() fixes both. It would also be good to reflect the "resumes on tab focus" reality in the JSDoc, since the ticker stop is not permanent in browsers.

  3. The error path can re-enter itself. _notifyAllSubscribers re-throws subscriber errors, so if an app callback throws while handling TOKEN_REFRESH_FAILED, the error lands in the tick's catch block, which increments the counter and calls _handleAutoRefreshFailure again: the event fires twice, and a second subscriber throw becomes an unhandled rejection out of the setInterval tick. Please make sure the failure handler cannot run twice for the same tick.

  4. Every error currently counts toward the limit, including non-retryable 4xx responses (revoked token, reuse detection, rate limits) that already have their own handling in _callRefreshToken (sign-out or session preserve). Since the feature targets an unreachable server, please gate the counting on isAuthRetryableFetchError(error) so terminal auth states and rate limits do not shut down auto-refresh. Also worth deciding: the event fires with a null session even when a still-valid session was preserved, and apps commonly treat a null session as signed out, so consider passing the current session or calling this out in the JSDoc.

On tests: the suite mocks both _useSession and _callRefreshToken, so it mostly exercises the plumbing between mocks. Could you add at least one test that goes through the real session load path (a fetch-level mock is fine)? None of the tests pass a custom lock either, so the legacy path is not actually covered yet despite the PR notes.

One thing you do not need to solve: adding TOKEN_REFRESH_FAILED to AuthChangeEvent has cross-SDK naming implications that we will coordinate on our side.

Really appreciate the care that went into this, the write-up alone made it a pleasure to review. Thank you for helping make Supabase better, contributions like this are what keep these libraries moving.

@pkg-pr-new

pkg-pr-new Bot commented Jul 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

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

@supabase/functions-js

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

@supabase/postgrest-js

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

@supabase/realtime-js

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

@supabase/storage-js

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

@supabase/supabase-js

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

commit: 420df5f

@coveralls

coveralls commented Jul 29, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 73.795% (-7.5%) from 81.305% — 7vignesh:feat/auth-max-refresh-failures into supabase:master

@7vignesh
7vignesh force-pushed the feat/auth-max-refresh-failures branch from f64ad7d to 420df5f Compare July 29, 2026 14:24
@7vignesh

Copy link
Copy Markdown
Author

Hi @mandarini, thank you for the thorough review! All 4 points addressed in the latest push:

1. Option flow-through via createClient
Added maxAutoRefreshFailures to SupabaseClientOptions['auth'] in packages/core/supabase-js/src/lib/types.ts, destructured and forwarded it in _initSupabaseAuthClient, and added two supabase-js unit tests verifying the value reaches the auth client (same pattern as lockAcquireTimeout).

2. Counter goes stale
Added this.autoRefreshFailureCount = 0 at the top of _startAutoRefresh(). This resets the budget on sign-in, tab refocus (_onVisibilityChanged calls _startAutoRefresh), or any explicit startAutoRefresh() call. Also updated the JSDoc to note that in browsers the stop is not permanent since visibility changes restart the ticker.

3. Re-entry guard
Added this.autoRefreshTicker !== null check to _handleAutoRefreshFailure() so it short-circuits if already stopped. Wrapped _notifyAllSubscribers in a try/catch so subscriber errors are swallowed with a console.error instead of propagating back into the tick's catch block. Added a test that verifies TOKEN_REFRESH_FAILED fires exactly once even when a subscriber throws.

4. Only count retryable errors
Gated all failure-count increments behind isAuthRetryableFetchError(error). Non-retryable 4xx responses (revoked token, rate limits) no longer count toward the limit. Added a dedicated test confirming non-retryable errors don't increment the counter.

Tests: 11 tests now covering default behavior, max limit, counter reset, non-retryable errors, thrown errors, retryable session-load errors, no-session without error, startAutoRefresh reset, legacy lock path, and subscriber re-entry. Plus 2 supabase-js flow-through tests. All pass.

@7vignesh
7vignesh requested a review from mandarini July 31, 2026 22:19

@mandarini mandarini left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @7vignesh! Sorry for the back and forth: my point 2 last round asked you to reset the counter in _startAutoRefresh(), and that was wrong advice. _onVisibilityChanged calls it on every tab refocus, so the budget refills constantly and the event would almost never fire in a browser.

Changes needed:

  1. Move the reset out of _startAutoRefresh() into _saveSession() and _removeSession() (next to the existing this.lastRefreshFailure = null). The reset in _callRefreshToken then becomes redundant.

  2. Make it recoverable off-browser. In React Native and Node nothing restarts the ticker after the limit is hit, not even signing back in. In _saveSession, after the reset:

    if (this.autoRefreshToken && this.autoRefreshTicker === null && !isBrowser()) {
      void this._startAutoRefresh()
    }
  3. Pass false as the third arg to _notifyAllSubscribers. It currently broadcasts, so other tabs emit TOKEN_REFRESH_FAILED while their own ticker is still running.

  4. Pass the session instead of null. _callRefreshToken preserves sessions whose access token is still valid, and (event, session) => setUser(session?.user ?? null) would log those users out.

  5. Move the caveats onto the public JSDoc for maxAutoRefreshFailures in both auth-js/src/lib/types.ts and supabase-js/src/lib/types.ts: counts ticks not network attempts (N x 30s), retryable errors only, and the tab refocus / off-browser behavior.

  6. Add one test that goes through the real path (seed storage, make the injected fetch reject) instead of mocking _useSession, and make nonRetryableError() a real error: new AuthApiError('Token revoked', 401, 'refresh_token_not_found'). A plain object has no __isAuthError brand, so that test currently passes for the wrong reason.

  7. pnpm nx format, line 41 of the new test file is over our 100 char width.

Your call on one thing: the two catch (e) blocks count without the retryable gate, so a storage adapter throw counts too. Gate them or document the counter as "failed ticks".

Thank you again for sticking with this, and sorry for the wrong turn on the reset location.

Add an opt-in maxAutoRefreshFailures option that stops the auto-refresh
ticker after N consecutive failed ticks and emits a TOKEN_REFRESH_FAILED
event via onAuthStateChange.

This lets applications detect when the auth server is permanently
unreachable and react accordingly (redirect to login, show a banner)
instead of retrying silently forever.

Defaults to 0 (no limit) so existing behavior is unchanged.

Closes supabase#1680
@7vignesh
7vignesh force-pushed the feat/auth-max-refresh-failures branch from 420df5f to 823388f Compare August 5, 2026 19:13
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@7vignesh

7vignesh commented Aug 5, 2026

Copy link
Copy Markdown
Author

Hi @mandarini, all 7 points addressed. Sorry for the churn on the earlier rounds, this push should be solid.

1. Reset location: Moved from _startAutoRefresh() to _saveSession() and _removeSession(). Removed the redundant reset in _callRefreshToken and the tick paths (since _saveSession is always called on successful refresh).

2. Recoverable off-browser: Added restart logic in _saveSession(): when the ticker is null and autoRefreshToken is enabled in a non-browser environment, _startAutoRefresh() is called so signing back in or setSession recovers auto-refresh.

3. No broadcast: Changed to _notifyAllSubscribers('TOKEN_REFRESH_FAILED', session, false) so other tabs don't emit the event while their own ticker is still running.

4. Pass session: _handleAutoRefreshFailure now accepts and passes the current session (from the tick callback) instead of null. Catch blocks outside the session scope pass null (default param).

5. JSDoc caveats: Updated both auth-js/src/lib/types.ts and supabase-js/src/lib/types.ts to document: counts ticks not network attempts (~30s each), retryable errors only, and tab refocus / off-browser recovery behavior.

6. Real-path test + real errors: Added a test that seeds storage directly and goes through the real __loadSession path (no _useSession mock). Changed nonRetryableError() to use a real AuthApiError('Token revoked', 401, 'refresh_token_not_found') with the proper __isAuthError brand.

7. Format: Ran nx format:write, all clean.

On the catch blocks: Left them counting all thrown errors (including storage adapter throws) since they represent genuinely failed ticks from the app's perspective. Documented the counter as "failed ticks" in the JSDoc.

13 tests passing, build green. Thanks for the patience and the detailed guidance!

@7vignesh
7vignesh requested a review from mandarini August 5, 2026 19:15
@zsspan

zsspan commented Aug 19, 2026

Copy link
Copy Markdown

@mandarini

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Always retry fetching

5 participants