chore(clerk-js): Improve session refresh retry logic - #5397
Conversation
🦋 Changeset detectedLatest commit: 5b382a3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
| response = await retry(() => fetch(urlStr, fetchOpts), { | ||
| // This retry handles only network errors, not 4xx or 5xx responses, | ||
| // so we want to try once immediately to handle simple network blips. | ||
| // Since fapiClient is responsible for the network layer only, | ||
| // callers need to use their own retry logic where needed. | ||
| retryImmediately: true, | ||
| // And then exponentially back off with a max delay of 3 seconds. | ||
| initialDelay: 700, | ||
| maxDelayBetweenRetries: 5000, | ||
| shouldRetry: (_: unknown, iterations: number) => { | ||
| // We want to retry only GET requests, as other methods are not idempotent. | ||
| return overwrittenRequestMethod === 'GET' && iterations < maxTries; | ||
| }, | ||
| }); |
| // This will retry the getToken call if it fails with a non-4xx error | ||
| // We're going to trigger 8 retries in the span of ~3 minutes, | ||
| // Example delays: 3s, 5s, 13s, 19s, 26s, 34s, 43s, 50s, total: ~193s | ||
| return retry(() => this._getToken(options), { | ||
| shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4, | ||
| factor: 1.55, | ||
| retryImmediately: false, | ||
| initialDelay: 3 * 1000, | ||
| maxDelayBetweenRetries: 50 * 1_000, | ||
| jitter: false, | ||
| shouldRetry: (error, iterationsCount) => { | ||
| return !is4xxError(error) && iterationsCount <= 8; | ||
| }, |
| }, INTERVAL_IN_MS); | ||
| const run = async () => { | ||
| await this.lock.acquireLockAndRun(cb); | ||
| this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS); |
| /** | ||
| * In most scenarios we want the poller to stop while we are fetching a fresh token during an outage. | ||
| * We want to avoid having the below `getToken()` retrying at the same time as the poller. | ||
| */ | ||
| this.#authService?.stopPollingForToken(); | ||
|
|
||
| // Attempt to grab a fresh token | ||
| await this.session | ||
| ?.getToken({ skipCache: true }) | ||
| // If the token fetch fails, let Clerk be marked as loaded and leave it up to the poller. | ||
| .catch(() => null) | ||
| .finally(() => { | ||
| this.#authService?.startPollingForToken(); | ||
| }); |
There was a problem hiding this comment.
Avoid parallel retries from this explicit invocation and the poller.
| const run = async () => { | ||
| await this.lock.acquireLockAndRun(cb); | ||
| this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS); | ||
| }; | ||
|
|
||
| void run(); |
There was a problem hiding this comment.
For reviewers:
This change allows for this behaviour
getToken -> 500 -> retry... retry.. retry... -> success/failure -> getToken
Until now, we had this one, which is no longer desired.
getToken -> 500 -> retry... retry.. retry... -> success/failure -> getToken
getToken -> 500 -> retry... retry.. retry... -> success/failure -> getToken
getToken -> 500 -> retry... retry.. retry... -> success/failure -> getToken
| // `/client` can fail with either a 401, a 403, 500 or network errors. | ||
| // 401 is already handled internally in our fetcher. | ||
| // 403 means that the client is blocked, signing out the user is the only option. | ||
| // 500 means that the client is not working, signing out the user is the only option, since the intention was to sign out the user. | ||
| if (isClerkAPIResponseError(err) && [403, 500].includes(err.status)) { |
There was a problem hiding this comment.
Why is the needed ?
When /client fails with 500, we attempt to read from __session and call /tokens. If /tokens returns with a 4xx handleUnauthencated gets called which tries to reload client which will fail with 500.
At this point the user cannot really do anything to resolve this, and clerk.js cannot auto recover. We set session: null to stop the poller from falling into an infinite loop.
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
…signs-inout # Conflicts: # packages/clerk-js/bundlewatch.config.json
| * Controls whether the helper should retry the operation immediately once before applying exponential backoff. | ||
| * The delay for the immediate retry is 100ms. | ||
| * @default true | ||
| * @default false |
There was a problem hiding this comment.
how come we changed the default here?
There was a problem hiding this comment.
We settled on it, as the best default to have.
|
!snapshot |
|
Hey @panteliselef - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact
npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact
npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact
npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact
npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact
npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact
npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact
npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact
npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact
npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact
npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact
npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact
npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact
npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact
npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact
npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact
npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact
npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact
npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact
npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact
npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact
npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact |
Description
This PR performs the following improvements related to how we are refreshing the session token.
getToken()call to have concluded before attempting to poll again. Example here.Clerk.handleUnauthenticated()will set session as null on 500 status code/clientresponse. Avoid infinite request loops, see here./clientfails on init, we stop the poller -> create the dummy client -> attempt manual request to/tokens-> start polling again.retryno longer fires immediately by default.Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change