|
1 | 1 | import { createCheckAuthorization } from '@clerk/shared/authorization'; |
2 | | -import { ClerkWebAuthnError, is4xxError, MissingExpiredTokenError } from '@clerk/shared/error'; |
| 2 | +import { isValidBrowserOnline } from '@clerk/shared/browser'; |
| 3 | +import { ClerkOfflineError, ClerkWebAuthnError, is4xxError, MissingExpiredTokenError } from '@clerk/shared/error'; |
3 | 4 | import { |
4 | 5 | convertJSONToPublicKeyRequestOptions, |
5 | 6 | serializePublicKeyCredentialAssertion, |
@@ -109,17 +110,41 @@ export class Session extends BaseResource implements SessionResource { |
109 | 110 |
|
110 | 111 | getToken: GetToken = async (options?: GetTokenOptions): Promise<string | null> => { |
111 | 112 | // This will retry the getToken call if it fails with a non-4xx error |
112 | | - // We're going to trigger 8 retries in the span of ~3 minutes, |
113 | | - // Example delays: 3s, 5s, 13s, 19s, 26s, 34s, 43s, 50s, total: ~193s |
114 | | - return retry(() => this._getToken(options), { |
115 | | - factor: 1.55, |
116 | | - initialDelay: 3 * 1000, |
117 | | - maxDelayBetweenRetries: 50 * 1_000, |
118 | | - jitter: false, |
119 | | - shouldRetry: (error, iterationsCount) => { |
120 | | - return !is4xxError(error) && iterationsCount <= 8; |
121 | | - }, |
122 | | - }); |
| 113 | + // For offline state, we use shorter retries (~15s total) before throwing ClerkOfflineError |
| 114 | + // For other errors, we retry up to 8 times over ~3 minutes |
| 115 | + try { |
| 116 | + const result = await retry(() => this._getToken(options), { |
| 117 | + factor: 1.55, |
| 118 | + initialDelay: 3 * 1000, |
| 119 | + maxDelayBetweenRetries: 50 * 1_000, |
| 120 | + jitter: false, |
| 121 | + shouldRetry: (error, iterationsCount) => { |
| 122 | + if (is4xxError(error)) { |
| 123 | + return false; |
| 124 | + } |
| 125 | + |
| 126 | + if (!isValidBrowserOnline()) { |
| 127 | + return iterationsCount <= 3; |
| 128 | + } |
| 129 | + return iterationsCount <= 8; |
| 130 | + }, |
| 131 | + }); |
| 132 | + |
| 133 | + // If we're offline and got a null/empty result, this is likely due to |
| 134 | + // BaseResource._baseFetch returning null when offline (silent failure mode). |
| 135 | + // Throw ClerkOfflineError to make the offline state explicit. |
| 136 | + if (!result && !isValidBrowserOnline()) { |
| 137 | + throw new ClerkOfflineError('Network request failed while offline. The browser appears to be disconnected.'); |
| 138 | + } |
| 139 | + |
| 140 | + return result; |
| 141 | + } catch (error) { |
| 142 | + // If the browser is offline after retries, throw ClerkOfflineError |
| 143 | + if (!isValidBrowserOnline()) { |
| 144 | + throw new ClerkOfflineError('Network request failed while offline. The browser appears to be disconnected.'); |
| 145 | + } |
| 146 | + throw error; |
| 147 | + } |
123 | 148 | }; |
124 | 149 |
|
125 | 150 | checkAuthorization: CheckAuthorization = params => { |
@@ -399,12 +424,14 @@ export class Session extends BaseResource implements SessionResource { |
399 | 424 | const params: Record<string, string | null> = template ? {} : { organizationId: organizationId ?? null }; |
400 | 425 | const lastActiveToken = this.lastActiveToken?.getRawString(); |
401 | 426 |
|
402 | | - return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined).catch(e => { |
| 427 | + const tokenResolver = Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined).catch(e => { |
403 | 428 | if (MissingExpiredTokenError.is(e) && lastActiveToken) { |
404 | 429 | return Token.create(path, { ...params }, { expired_token: lastActiveToken }); |
405 | 430 | } |
406 | 431 | throw e; |
407 | 432 | }); |
| 433 | + |
| 434 | + return tokenResolver; |
408 | 435 | } |
409 | 436 |
|
410 | 437 | #dispatchTokenEvents(token: TokenResource, shouldDispatch: boolean): void { |
|
0 commit comments