Skip to content

Keep the current auth token when a refetch before expiry fails - #210

Closed
sindreij wants to merge 1 commit into
convex:mainfrom
sindreij:keep-token-on-failed-refetch
Closed

sindreij wants to merge 1 commit into
convex:mainfrom
sindreij:keep-token-on-failed-refetch

Conversation

@sindreij

@sindreij sindreij commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

This is created by my agent (running Opus 5.5), so please feel free to ignore if it does not make sense

Problem

scheduleTokenRefetch starts refetchToken() refreshTokenLeewaySeconds before the token expires. When fetchToken({ forceRefreshToken: true }) returns null, refetchToken() calls clearAuth() straight away. That sends Authenticate with tokenType: "None" over the open socket, so the server re-runs every active query without an identity, and auth-required queries all fail together. The token the server holds is still valid at that point.

We see this in production with a React Native app, where token requests often fail for a few seconds on mobile networks. In one 3-hour window there were 39 such events, most failing more than 20 queries in the same second, roughly 300 a day. The app recovers once a later token request succeeds, but every subscribed query errors in between.

tryToReauthenticate() handles the same null differently: it stops the socket before it fetches.

Change

When a refetch returns null and the current token still has at least 2 s left (by its exp), keep the token and schedule another refetch. Each retry waits half the remaining lifetime, capped at 10 s. Once the token has less than 2 s left, or there is no token or it can't be decoded, the behaviour is as before: clearAuth() and onAuthChange(false).

  • With the default 10 s leeway that is up to three retries. With 60 s, it's about eight.
  • The retry timer is stored in the waitingForScheduledRefetch state, so setConfig, stop() and tryToReauthenticate() cancel it like the normal scheduled refetch.
  • Remaining lifetime uses the client clock. If that clock is behind, the server rejects the token first and tryToReauthenticate() takes over, as today.

Open questions

  • Semantics of null. The setAuth docs describe null as "the token cannot be retrieved", with permanent revocation as an example. With this change, a null from a refetch before expiry signs the client out up to the leeway later rather than immediately. Explicit sign-out (clearAuth, or ConvexProviderWithAuth seeing isAuthenticated: false) is unaffected. If you'd rather keep null meaning "sign out now", an alternative is a distinct signal for transient failures. That could be a new return value or option, or treating a thrown fetchToken as transient: today a throw from the scheduled refetch is an unhandled rejection.
  • Retry cadence. The 10 s cap and 2 s floor are guesses; I'm happy to tie them to refreshTokenLeewaySeconds instead.

Testing

  • New unit test, src/browser/sync/authentication_manager.test.ts, using the real LocalSyncState with fake timers. A refetch that fails before expiry keeps the token and picks up a later fresh one. A refetch that keeps failing clears auth only in the last seconds before expiry. Both tests fail on main. I know your main test suite isn't in this repository, so move or drop the file as you prefer.
  • Against a real deployment: a Node ConvexClient built from this branch's source, with authRefreshTokenLeewaySeconds: 840 and a fetchToken that returns null when forced. It kept its token, retried every 10 s, and had no query failures over 130 s. On main, both subscribed auth-required queries failed when the refetch ran.
  • npm run typecheck, eslint and prettier -c pass on the changed files.

🤖 Generated with Claude Code

A scheduled refetch runs refreshTokenLeewaySeconds before the token
expires. When fetchToken returned null, refetchToken cleared auth at
once on the open socket, so the server re-ran every query without an
identity while the token was still valid.

Keep the token and retry, waiting at most 10 s between attempts, until
it has less than 2 s left; then clear auth as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sindreij

Copy link
Copy Markdown
Contributor Author

Closing: we've heard you're working on something similar. Thanks!

@sindreij sindreij closed this Oct 27, 2026
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.

1 participant