chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

chore(clerk-js): Improve session refresh retry logic - #5397

Merged
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout
Mar 27, 2025
Merged

chore(clerk-js): Improve session refresh retry logic#5397
panteliselef merged 13 commits into
mainfrom
elef/sdki-949-startstop-session-poller-when-signs-inout

Conversation

@panteliselef

@panteliselefpanteliselef commented Mar 19, 2025

Copy link
Copy Markdown
Contributor

Description

This PR performs the following improvements related to how we are refreshing the session token.

  1. Switches from an interval to a timeout. From a parallel behaviour we are switching to a sequential behaviour which allows all retries of particular getToken() call to have concluded before attempting to poll again. Example here.
  2. Clerk.handleUnauthenticated() will set session as null on 500 status code /client response. Avoid infinite request loops, see here.
  3. If /client fails on init, we stop the poller -> create the dummy client -> attempt manual request to /tokens -> start polling again.
  4. retry no longer fires immediately by default.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@panteliselefpanteliselef self-assigned this Mar 19, 2025
@changeset-bot

changeset-botBot commented Mar 19, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5b382a3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@clerk/clerk-jsMinor
@clerk/sharedMinor
@clerk/chrome-extensionPatch
@clerk/clerk-expoPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/vuePatch

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

@vercel

vercelBot commented Mar 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

NameStatusPreviewCommentsUpdated (UTC)
clerk-js-sandbox✅ Ready (Inspect)Visit Preview💬 Add feedbackMar 27, 2025 1:57pm

Comment on lines +235 to +248
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;
},
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +95 to +106
// 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;
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

}, INTERVAL_IN_MS);
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Credits to @nikosdouvlis

Comment on lines +2070 to +2083
/**
* 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();
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Avoid parallel retries from this explicit invocation and the poller.

Comment on lines +18 to +23
const run = async () => {
await this.lock.acquireLockAndRun(cb);
this.timerId = this.workerTimers.setTimeout(run, INTERVAL_IN_MS);
};

void run();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Comment on lines +1695 to +1699
// `/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)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadpackages/clerk-js/src/core/fapiClient.ts Outdated
…poller-when-signs-inout' into elef/sdki-949-startstop-session-poller-when-signs-inout
@panteliselef
panteliselef marked this pull request as ready for review March 21, 2025 16:30
@panteliselef
panteliselef requested a review from a teamMarch 21, 2025 16:30
…signs-inout
# Conflicts:
#	packages/clerk-js/bundlewatch.config.json
Comment threadpackages/clerk-js/src/core/resources/Session.ts Outdated
* 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how come we changed the default here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We settled on it, as the best default to have.

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/agent-toolkit0.0.16-snapshot.v20250327150215
@clerk/astro2.4.5-snapshot.v20250327150215
@clerk/backend1.25.8-snapshot.v20250327150215
@clerk/chrome-extension2.2.23-snapshot.v20250327150215
@clerk/clerk-js5.59.0-snapshot.v20250327150215
@clerk/elements0.23.8-snapshot.v20250327150215
@clerk/clerk-expo2.9.6-snapshot.v20250327150215
@clerk/expo-passkeys0.2.0-snapshot.v20250327150215
@clerk/express1.3.59-snapshot.v20250327150215
@clerk/fastify2.1.32-snapshot.v20250327150215
@clerk/localizations3.13.4-snapshot.v20250327150215
@clerk/nextjs6.12.12-snapshot.v20250327150215
@clerk/nuxt1.4.6-snapshot.v20250327150215
@clerk/clerk-react5.25.5-snapshot.v20250327150215
@clerk/react-router1.1.11-snapshot.v20250327150215
@clerk/remix4.5.11-snapshot.v20250327150215
@clerk/shared3.3.0-snapshot.v20250327150215
@clerk/tanstack-react-start0.12.2-snapshot.v20250327150215
@clerk/testing1.4.33-snapshot.v20250327150215
@clerk/themes2.2.26-snapshot.v20250327150215
@clerk/types4.50.1-snapshot.v20250327150215
@clerk/vue1.4.5-snapshot.v20250327150215

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.0.16-snapshot.v20250327150215 --save-exact

@clerk/astro

npm i @clerk/astro@2.4.5-snapshot.v20250327150215 --save-exact

@clerk/backend

npm i @clerk/backend@1.25.8-snapshot.v20250327150215 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@2.2.23-snapshot.v20250327150215 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.59.0-snapshot.v20250327150215 --save-exact

@clerk/elements

npm i @clerk/elements@0.23.8-snapshot.v20250327150215 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@2.9.6-snapshot.v20250327150215 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@0.2.0-snapshot.v20250327150215 --save-exact

@clerk/express

npm i @clerk/express@1.3.59-snapshot.v20250327150215 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.1.32-snapshot.v20250327150215 --save-exact

@clerk/localizations

npm i @clerk/localizations@3.13.4-snapshot.v20250327150215 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@6.12.12-snapshot.v20250327150215 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@1.4.6-snapshot.v20250327150215 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.25.5-snapshot.v20250327150215 --save-exact

@clerk/react-router

npm i @clerk/react-router@1.1.11-snapshot.v20250327150215 --save-exact

@clerk/remix

npm i @clerk/remix@4.5.11-snapshot.v20250327150215 --save-exact

@clerk/shared

npm i @clerk/shared@3.3.0-snapshot.v20250327150215 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@0.12.2-snapshot.v20250327150215 --save-exact

@clerk/testing

npm i @clerk/testing@1.4.33-snapshot.v20250327150215 --save-exact

@clerk/themes

npm i @clerk/themes@2.2.26-snapshot.v20250327150215 --save-exact

@clerk/types

npm i @clerk/types@4.50.1-snapshot.v20250327150215 --save-exact

@clerk/vue

npm i @clerk/vue@1.4.5-snapshot.v20250327150215 --save-exact

@panteliselef
panteliselef merged commit e984494 into mainMar 27, 2025
@panteliselef
panteliselef deleted the elef/sdki-949-startstop-session-poller-when-signs-inout branch March 27, 2025 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@panteliselef@clerk-cookie@brkalow@nikosdouvlis