Skip to content

fix(auth): keep the session after refreshing it on initialize - #1647

Open
hsusul wants to merge 1 commit into
supabase:mainfrom
hsusul:fix/auth-recover-keeps-refreshed-session
Open

hsusul wants to merge 1 commit into
supabase:mainfrom
hsusul:fix/auth-recover-keeps-refreshed-session

Conversation

@hsusul

@hsusul hsusul commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix.

What is the current behavior?

Fixes #1646

_recover_and_refresh (called by initialize_from_storage()) doesn't return after a successful refresh:

try:
    await self._call_refresh_token(refresh_token)  # saves the new session
    self._network_retries = 0
except Exception as e:
    ...
await self._remove_session()  # reached on success too
return

When the persisted session's access token has expired, the client refreshes it. _save_session stores the new session and starts the auto-refresh timer, and TOKEN_REFRESHED is emitted. Control then falls through to _remove_session(), which deletes the new session and cancels the timer. Any app restarted after the access token expired (1 hour by default) signs the user out, even though the refresh succeeded. Listeners also got TOKEN_REFRESHED for a session that no longer exists.

What is the new behavior?

_recover_and_refresh returns as soon as the refresh succeeds. Failures behave as before:

  • A retryable error still schedules a retry.
  • A non-retryable error, or running out of retries, still removes the session.
  • With auto-refresh disabled or no refresh token, the session is still removed.

This matches auth-js's _recoverAndRefresh, which only drops the session when the refresh returns an error.

SyncGoTrueClient has the same code (it's generated by build-sync), so it gets the same one-line change.

Tests

test_initialize_from_storage_keeps_refreshed_session in tests/_async/test_gotrue.py, plus its unasync-generated twin in tests/_sync/test_gotrue.py. Each stores an expired session, serves /token from an httpx.MockTransport, calls initialize_from_storage(), and asserts:

  • TOKEN_REFRESHED was emitted;
  • the refreshed session is still in storage;
  • the auto-refresh timer is still running.

Both fail on main (stored is None) and pass here. They don't need the GoTrue container.

Validation

  • The two new tests: 2 passed (2 failed with the upstream client code and the same test files).
  • uv run --package supabase_auth pytest src/auth/tests/test_helpers.py: 22 passed.
  • uv run --package supabase_auth mypy src/auth/src/supabase_auth src/auth/tests: no issues in 36 source files.
  • uv run ruff check --fix / uv run ruff format: clean.
  • git diff --check: clean.

The rest of src/auth/tests needs the GoTrue container from make auth.start-infra (Docker), which wasn't running. The full suite gives 44 passed and 122 failed with this change, versus 42 passed and 124 failed on main. All 122 remaining failures are httpx.ConnectError.

_recover_and_refresh did not return after a successful refresh, so it
fell through to _remove_session(). An expired-but-refreshable persisted
session was refreshed, TOKEN_REFRESHED was emitted, and then the new
session was deleted from storage and its auto-refresh timer cancelled,
signing the user out on every restart after the access token expired.

Return once the refresh succeeds. The session is still removed when the
refresh fails with a non-retryable error or retries are exhausted.
Applies to the async client and its generated sync twin.

Fixes supabase#1646
@hsusul
hsusul requested review from a team and o-santi as code owners September 17, 2026 16:21

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

Development

Successfully merging this pull request may close these issues.

initialize_from_storage removes the session right after refreshing it

1 participant