Skip to content

fix(clerk-js): Do not treat 429 responses as authentication failures#8004

Merged
brkalow merged 11 commits intomainfrom
bryce/fix-429-unauthenticated-handling
Mar 7, 2026
Merged

fix(clerk-js): Do not treat 429 responses as authentication failures#8004
brkalow merged 11 commits intomainfrom
bryce/fix-429-unauthenticated-handling

Conversation

@brkalow
Copy link
Member

@brkalow brkalow commented Mar 6, 2026

Summary

429 (Too Many Requests) responses were being caught by is4xxError and triggering handleUnauthenticated, which signs the user out. This caused a cascading failure pattern observed in production telemetry for a customer.

  1. Token fetch fails with a legitimate 4xx (e.g. authentication_invalid)
  2. handleUnauthenticated fires and calls /touch to recover
  3. /touch returns 429 (rate limited)
  4. 429 triggers handleUnauthenticated again (treated as auth failure)
  5. Each retry hits 429 → infinite loop (30+ retries in 11 seconds observed)
  6. User gets forcibly redirected to sign-in

Changes

  • @clerk/shared: Add is429Error helper alongside existing is4xxError
  • AuthCookieService.handleGetTokenError: Exclude 429 from triggering handleUnauthenticated — fall through to degraded status instead
  • Clerk.setActive: Exclude 429 from triggering handleUnauthenticated, and also don't re-throw (swallow like other transient errors)
  • Clerk.#touchCurrentSession: Exclude 429 from triggering handleUnauthenticated
  • Session.getToken retry logic: Allow 429 to be retried with exponential backoff instead of immediately giving up (was blocked by is4xxError in shouldRetry)

Telemetry evidence (7-day window)

Event Count
handleUnauthenticated triggered 650 (285 unique sessions)
/touch returning 429 Majority of request failed errors during cascades
Top offending session 59 handleUnauthenticated events from a single session

Test plan

  • is429Error unit tests pass (true for 429, false for all other statuses including null/undefined)
  • is4xxError unit tests pass (still returns true for 429 — no breaking change)
  • All 59 Session tests pass
  • All 488 clerk-js core tests pass (36 test files)
  • Verify in staging that 429 from /touch no longer triggers sign-out
  • Monitor telemetry for reduction in handleUnauthenticated triggered events

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed handling of rate-limit (HTTP 429) responses so they no longer trigger sign-out; clients now enter a degraded state and retry, preserving signed-in sessions and avoiding recursive unauthenticated loops.
  • Tests

    • Added resiliency tests covering 429 rate-limit scenarios and unauthenticated-edge cases to ensure stable runtime behavior under high traffic.

429 (Too Many Requests) responses were being caught by `is4xxError`
and triggering `handleUnauthenticated`, which signs the user out.
This caused a cascading failure: the unauthenticated flow calls
`/touch`, which returns 429 due to rate limiting, which triggers
`handleUnauthenticated` again — creating an infinite retry loop
that forces users to re-authenticate even though their session is
still valid.

Changes:
- Add `is429Error` helper to `@clerk/shared`
- Exclude 429 from triggering `handleUnauthenticated` in
  `AuthCookieService.handleGetTokenError`, `Clerk.setActive`,
  and `Clerk.#touchCurrentSession`
- Allow 429 to be retried in `Session.getToken` instead of
  immediately failing (was blocked by `is4xxError` check in
  `shouldRetry`)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Mar 6, 2026 9:03pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: f11e4d4

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

This PR includes changesets to release 21 packages
Name Type
@clerk/clerk-js Patch
@clerk/shared Patch
@clerk/chrome-extension Patch
@clerk/expo Patch
@clerk/agent-toolkit Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/ui Patch
@clerk/vue Patch

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 6, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@8004

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8004

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8004

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8004

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8004

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8004

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8004

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8004

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8004

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8004

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8004

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8004

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8004

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8004

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8004

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8004

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8004

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8004

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8004

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8004

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8004

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8004

commit: f11e4d4

…elper

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>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds two predicates, is429Error and isUnauthenticatedError, to shared error helpers and re-exports them. Call sites in AuthCookieService, clerk.ts, and Session.ts now use isUnauthenticatedError instead of a generic 4xx check so HTTP 429 responses are treated as rate-limited (degraded/retryable). A changeset documenting the fix was added. Integration tests were added for 429 resiliency and related unauthenticated scenarios.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: preventing 429 rate-limit responses from being treated as authentication failures, which is the core fix across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/clerk-js/src/core/clerk.ts`:
- Around line 1599-1602: setActive can still fail when newSession?.getToken()
throws a 429 because the current catch rethrows 429; change the getToken catch
so 429 is not rethrown — treat 429 like the throttling case and swallow it (or
route to the same non-throw handling) instead of throwing. Concretely, update
the catch around newSession?.getToken() in setActive/__internal_touch so that
you only rethrow for errors that are neither 4xx nor is429Error(e) (use
is4xxError(e) and is429Error(e) checks) and call this.handleUnauthenticated()
for 4xx errors as before; ensure is429Error(e) branch does not rethrow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: ac8cf484-1b7b-4811-9fc5-d1ebe64b67f2

📥 Commits

Reviewing files that changed from the base of the PR and between 79d0ecf and 3a4ea45.

📒 Files selected for processing (7)
  • .changeset/fix-429-unauthenticated.md
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/resources/Session.ts
  • packages/shared/src/__tests__/error.spec.ts
  • packages/shared/src/error.ts
  • packages/shared/src/errors/helpers.ts

…tests

- Let 429 errors from touch propagate to the caller in setActive instead
  of silently swallowing, so developers can handle rate limits (e.g. show
  retry UI using error.retryAfter)
- Add integration tests for 429 resiliency: user stays signed in, status
  goes to degraded (not error), and no recursive handleUnauthenticated loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "status transitions to degraded" test was unreliable because 429
on /tokens triggers retry logic (up to 8 retries) before surfacing
the error to handleGetTokenError. The remaining 3 tests cover the
critical app-level behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…th failures

Only 401 (invalid/expired session) and 422 (invalid session state)
should trigger the unauthenticated flow. Other 4xx codes are excluded:
- 400: bad request, not an auth failure
- 403: authorization issue, session is still valid
- 404: ambiguous (session not found vs org/template not found share
  the same resource_not_found code)
- 429: transient rate limit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shouldRetry logic was changed from is4xxError to
isUnauthenticatedError, which inadvertently caused deterministic
client errors (400, 403, 404) to retry up to 8 times over ~3 minutes.
Restore the correct behavior: only retry on transient failures (5xx,
network errors, 429 rate limits). Also update changeset description.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "remains signed in" test passes even without the fix due to a race
condition — handleUnauthenticated is fire-and-forget and may not complete
within the wait window. The remaining two tests (setActive surfaces error,
no recursive handleUnauthenticated) are deterministic and sufficient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
*/
export function isUnauthenticatedError(e: any): boolean {
const status = e?.status;
return status === 401 || status === 422;
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

Copy link
Member

@jacekradko jacekradko left a comment

Choose a reason for hiding this comment

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

I like it! Definitely a more correct approach to handling various 4XX status codes.

@brkalow brkalow merged commit 7fb870d into main Mar 7, 2026
41 checks passed
@brkalow brkalow deleted the bryce/fix-429-unauthenticated-handling branch March 7, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants