Skip to content

Commit 06e16c9

Browse files
brkalowclaude
andcommitted
refactor: Replace is4xxError+is429Error with isUnauthenticatedError helper
Introduce isUnauthenticatedError that encapsulates the "4xx but not 429" logic in one place, making the intent self-documenting and preventing future callers from accidentally treating rate limits as auth failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a4ea45 commit 06e16c9

6 files changed

Lines changed: 49 additions & 13 deletions

File tree

packages/clerk-js/src/core/auth/AuthCookieService.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import { clerkEvents } from '@clerk/shared/clerkEventBus';
44
import type { createCookieHandler } from '@clerk/shared/cookie';
55
import { setDevBrowserInURL } from '@clerk/shared/devBrowser';
66
import {
7-
is429Error,
8-
is4xxError,
97
isClerkAPIResponseError,
108
isClerkRuntimeError,
119
isNetworkError,
10+
isUnauthenticatedError,
1211
} from '@clerk/shared/error';
1312
import type { Clerk, InstanceType } from '@clerk/shared/types';
1413
import { noop } from '@clerk/shared/utils';
@@ -221,9 +220,7 @@ export class AuthCookieService {
221220
return;
222221
}
223222

224-
// 429 (rate limited) is not an auth failure — the session may still be valid.
225-
// Treat it the same as a transient error and degrade gracefully.
226-
if (is4xxError(e) && !is429Error(e)) {
223+
if (isUnauthenticatedError(e)) {
227224
void this.clerk.handleUnauthenticated().catch(noop);
228225
return;
229226
}

packages/clerk-js/src/core/clerk.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {
55
ClerkRuntimeError,
66
EmailLinkError,
77
EmailLinkErrorCodeStatus,
8-
is429Error,
98
is4xxError,
109
isClerkAPIResponseError,
1110
isClerkRuntimeError,
11+
isUnauthenticatedError,
1212
} from '@clerk/shared/error';
1313
import {
1414
disabledAllAPIKeysFeatures,
@@ -1596,9 +1596,11 @@ export class Clerk implements ClerkInterface {
15961596
this.updateClient(updatedClient, { __internal_dangerouslySkipEmit: true });
15971597
}
15981598
} catch (e) {
1599-
if (is4xxError(e) && !is429Error(e)) {
1599+
if (isUnauthenticatedError(e)) {
16001600
void this.handleUnauthenticated();
1601-
} else if (!is429Error(e)) {
1601+
} else if (!is4xxError(e)) {
1602+
// Swallow 4xx errors like 429 (rate limit) that are not auth failures.
1603+
// Non-4xx errors (5xx, network) should still propagate.
16021604
throw e;
16031605
}
16041606
}
@@ -3175,7 +3177,7 @@ export class Clerk implements ClerkInterface {
31753177
}
31763178

31773179
await session.touch().catch(e => {
3178-
if (is4xxError(e) && !is429Error(e)) {
3180+
if (isUnauthenticatedError(e)) {
31793181
void this.handleUnauthenticated();
31803182
}
31813183
});

packages/clerk-js/src/core/resources/Session.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { isValidBrowserOnline } from '@clerk/shared/browser';
33
import {
44
ClerkOfflineError,
55
ClerkWebAuthnError,
6-
is429Error,
7-
is4xxError,
6+
isUnauthenticatedError,
87
MissingExpiredTokenError,
98
} from '@clerk/shared/error';
109
import {
@@ -162,8 +161,7 @@ export class Session extends BaseResource implements SessionResource {
162161
maxDelayBetweenRetries: 50 * 1_000,
163162
jitter: false,
164163
shouldRetry: (error, iterationsCount) => {
165-
// 429 is a rate limit, not an auth error — retry with backoff
166-
if (is4xxError(error) && !is429Error(error)) {
164+
if (isUnauthenticatedError(error)) {
167165
return false;
168166
}
169167

packages/shared/src/__tests__/error.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
is429Error,
99
is4xxError,
1010
isClerkRuntimeError,
11+
isUnauthenticatedError,
1112
} from '../error';
1213

1314
describe('ErrorThrower', () => {
@@ -101,6 +102,27 @@ describe('is429Error', () => {
101102
});
102103
});
103104

105+
describe('isUnauthenticatedError', () => {
106+
it('returns true for auth-related 4xx errors', () => {
107+
expect(isUnauthenticatedError({ status: 400 })).toBe(true);
108+
expect(isUnauthenticatedError({ status: 401 })).toBe(true);
109+
expect(isUnauthenticatedError({ status: 403 })).toBe(true);
110+
expect(isUnauthenticatedError({ status: 404 })).toBe(true);
111+
expect(isUnauthenticatedError({ status: 422 })).toBe(true);
112+
});
113+
114+
it('returns false for 429 (rate limit)', () => {
115+
expect(isUnauthenticatedError({ status: 429 })).toBe(false);
116+
});
117+
118+
it('returns false for non-4xx errors', () => {
119+
expect(isUnauthenticatedError({ status: 200 })).toBe(false);
120+
expect(isUnauthenticatedError({ status: 500 })).toBe(false);
121+
expect(isUnauthenticatedError({})).toBe(false);
122+
expect(isUnauthenticatedError(null)).toBe(false);
123+
});
124+
});
125+
104126
describe('ClerkOfflineError', () => {
105127
it('is an instance of ClerkRuntimeError', () => {
106128
const error = new ClerkOfflineError('Network request failed');

packages/shared/src/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
isPasswordPwnedError,
2828
isPasswordCompromisedError,
2929
isReverificationCancelledError,
30+
isUnauthenticatedError,
3031
isUnauthorizedError,
3132
isUserLockedError,
3233
} from './errors/helpers';

packages/shared/src/errors/helpers.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ export function is429Error(e: any): boolean {
4646
return e?.status === 429;
4747
}
4848

49+
/**
50+
* Checks if the provided error indicates the user's session is no longer valid
51+
* and should trigger the unauthenticated flow (e.g. sign-out / redirect to sign-in).
52+
*
53+
* This is a 4xx client error that is NOT a rate limit (429). Rate-limited requests
54+
* are transient — the session may still be valid, so they should be retried rather
55+
* than treated as authentication failures.
56+
*
57+
* Use this instead of `is4xxError` when deciding whether to call `handleUnauthenticated`.
58+
*
59+
* @internal
60+
*/
61+
export function isUnauthenticatedError(e: any): boolean {
62+
return is4xxError(e) && !is429Error(e);
63+
}
64+
4965
/**
5066
* Checks if the provided error is a network error.
5167
*

0 commit comments

Comments
 (0)