Skip to content

fix(auth): allow bounded retry after transient refresh failure on resume - #2769

Open
Shoryamishra61 wants to merge 1 commit into
supabase:masterfrom
Shoryamishra61:fix/2767-auth-refresh-recovery
Open

Shoryamishra61 wants to merge 1 commit into
supabase:masterfrom
Shoryamishra61:fix/2767-auth-refresh-recovery

Conversation

@Shoryamishra61

Copy link
Copy Markdown

🔍 Description

What changed?

  • Added _maybeRecoverOnStartAutoRefresh() to GoTrueClient which resets this.lastRefreshFailure upon an intentional startAutoRefresh() call on resume, provided:
    1. The client is not currently running internal initialization (!this._isInitializing).
    2. The cached failure is a transient transport error (isAuthRetryableFetchError).
    3. The recovery attempt is rate-bounded by REFRESH_FAILURE_COOLDOWN_MS (tracked via lastRecoveryAttemptAt), preventing repeated resume events or offline fluttering from triggering a retry storm.
  • Resets lastRecoveryAttemptAt to 0 upon successful token rotation and session teardown (_removeSession).
  • Added a comprehensive deterministic test suite (test/GoTrueClient.refreshRecovery.test.ts) covering 16 reproduction, cooldown, token rotation, concurrency, and security scenarios with offline mocks.

Why was this change needed?

In mobile / React Native environments, applications respond to AppState transitions (active) by calling supabase.auth.startAutoRefresh(). When an app starts or attempts to refresh while offline, _callRefreshToken records the failure in lastRefreshFailure with a 60-second cooldown (REFRESH_FAILURE_COOLDOWN_MS). If network connectivity restores within that 60-second window, subsequent resume events and scheduled ticks immediately encounter the cooldown cache and return the cached AuthRetryableFetchError without sending a single /token request. Users were left waiting up to 60 seconds before authentication could recover.

Closes #2767

📸 Screenshots/Examples

import { AppState } from 'react-native'

// When app transitions to foreground after connectivity restore,
// startAutoRefresh() now safely allows a bounded retry of the cached transient failure.
AppState.addEventListener('change', (state) => {
  if (state === 'active') {
    supabase.auth.startAutoRefresh()
  } else {
    supabase.auth.stopAutoRefresh()
  }
})

🔄 Breaking changes

  • This PR contains no breaking changes

📋 Checklist

  • I have read the Contributing Guidelines
  • My PR title follows the conventional commit format: <type>(<scope>): <description>
  • I have run pnpm nx format to ensure consistent code formatting
  • I have added tests for new functionality (if applicable)
  • I have updated documentation (if applicable)

📝 Additional notes

Root Cause

_callRefreshToken caches both retryable and non-retryable failures in lastRefreshFailure for REFRESH_FAILURE_COOLDOWN_MS (60s) to prevent proactive refresh storms. While this protects against runaway retries during an outage, React Native has no browser-style online event to clear the cooldown; instead, mobile applications use startAutoRefresh() on app foregrounding. Previously, startAutoRefresh() did not clear lastRefreshFailure, causing the client to return the cached failure for up to 60 seconds even after network was fully restored.

Relationship to other PRs

Concurrency & Security Invariants

  • Single-flight deduplication via refreshingDeferred ensures concurrent callers join the single in-flight network request.
  • Storage commit guards and _sessionRemovalEpoch prevent session resurrection on concurrent signOut or sign-in.
  • Non-retryable authentication errors (e.g., invalid_grant) are never cleared on resume.

In mobile / React Native environments, AppState transitions trigger
supabase.auth.startAutoRefresh(). When an app initializes or
foregrounds while offline, _callRefreshToken caches transient network
failures in lastRefreshFailure for REFRESH_FAILURE_COOLDOWN_MS (60s).
When connectivity is restored, subsequent foreground events or ticks
within 60s hit this cooldown cache, returning the cached error without
sending a single /token request.

This change allows startAutoRefresh() to clear lastRefreshFailure on
an intentional resume transition if the cached failure is retryable
(isAuthRetryableFetchError), bounded by lastRecoveryAttemptAt so rapid
fluttering or sustained offline resume events cannot cause a retry
storm. Non-retryable errors (e.g. invalid tokens) and internal
initialization remain strictly protected by the cooldown.

Closes supabase#2767
@Shoryamishra61
Shoryamishra61 requested review from a team as code owners October 16, 2026 06:12
Copilot AI balanced review requested due to automatic review settings October 16, 2026 06:12

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 Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4173e375-056d-4750-8cee-457740a88805
📥 Commits

Reviewing files that changed from the base of the PR and between 780a214 and b2df4f3.

📒 Files selected for processing: 2
  • packages/core/auth-js/src/GoTrueClient.ts
  • packages/core/auth-js/test/GoTrueClient.refreshRecovery.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved session recovery after temporary refresh failures, allowing eligible retries after a cooldown.
    • Prevented recovery attempts from interfering with initialization or overlapping refresh requests.
    • Reset recovery tracking after successful refreshes and session removal.
    • Added coverage for refresh recovery, invalid tokens, session changes, auto-refresh controls, and storage behavior.

GoTrueClient now tracks initialization state and the time of its most recent recovery attempt. When initialization is inactive, a cached failure is retryable, and the cooldown has elapsed, startAutoRefresh() clears the cached failure before starting auto-refresh. Successful refreshes and session removal reset the recovery timestamp. New tests cover recovery and related refresh behavior.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant GoTrueClient
  participant TokenEndpoint
  App->>GoTrueClient: startAutoRefresh()
  GoTrueClient->>GoTrueClient: Check initialization, retryability, and cooldown
  GoTrueClient->>GoTrueClient: Clear eligible cached failure
  GoTrueClient->>TokenEndpoint: Start auto-refresh token request
Loading

Assessment against linked issues

Objective Addressed Explanation
Provide a way to retry a cached retryable refresh failure after connectivity returns [#2767] ✅

Priority: ➖ Normal

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to b2df4

Intentional auto-refresh restart appears to retry promptly after connectivity returns. No actionable merge blocker was established.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@orangec-at

Copy link
Copy Markdown

Tested this on React Native (Expo SDK 58, RN 0.88, Hermes, iOS 26.5 simulator), A/B against v2.118.0. Stored session marked expired first, offline simulated in fetch. Repro app and raw logs: https://github.com/orangec-at/supabase-rn-auth-lab

A. Offline refresh fails (8 attempts, 25.5 s) → network back → startAutoRefresh()

  • v2.118.0: 0 /token requests, recovers 62.2 s after the network came back
  • this PR: 1 request, recovers 2.2 s after the network came back ✅

B. Same, but the app returns to the foreground while still offline (AppState → startAutoRefresh()), and the network comes back after that call's retries have failed → startAutoRefresh() again

  • v2.118.0: recovers 32.2 s after the network came back
  • this PR: recovers 57.4 s after the network came back

In B the offline startAutoRefresh() uses the one recovery attempt. Its retries fail, which caches a new failure and restarts the cooldown, and the call made once the network is back is blocked by lastRecoveryAttemptAt. On mobile, foreground-while-offline is a common path (subway, elevator), so B may be worth a test case. If the network returns while that retry loop is still running, the next backoff step recovers, so the worst case is the one above.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

3 participants