Skip to content

Fix duplicate /users/me requests during console sign-in - #40

Open
PasinduYeshan wants to merge 1 commit into
thunder-id:mainfrom
PasinduYeshan:fix/4115-duplicate-users-me
Open

Fix duplicate /users/me requests during console sign-in#40
PasinduYeshan wants to merge 1 commit into
thunder-id:mainfrom
PasinduYeshan:fix/4115-duplicate-users-me

Conversation

@PasinduYeshan

@PasinduYeshan PasinduYeshan commented Jul 27, 2026

Copy link
Copy Markdown

Fixes thunder-id/thunderid#4115 — a single console sign-in issued ~10 GET /users/me (measured: 9 and 12) instead of 1.

Two independent defects, measured separately at runtime.

1. ProtectedRoute retried its own render, re-issuing sign-in each time

signIn() was called from the render body, which updates ThunderIDProvider state during render:

Cannot update a component (ThunderIDProvider) while rendering a different component (ProtectedRoute)

Worse, the throw new ThunderIDRuntimeError('ProtectedRoute misconfiguration.', ...) sat after the if (!isSignedIn) block and no branch inside returned. Since isSignedIn is already known false at that point, it fired on every unauthenticated render, including ones the component had just handled correctly (signInUrl, onSignIn, and the default sign-in all fell through to it).

React discarded each throw and re-rendered the root synchronously (11× There was an error during concurrent rendering but React was able to recover), and every retry re-ran the render-phase signIn(). That loop is the request burst. It self-terminated once auth state settled, which is why the bug was invisible except in the network tab.

Decisions:

  • Sign-in moved into useEffect, guarded by a ref so it fires once per unauthenticated state. The ref resets when the state leaves "unauthenticated", so a later sign-out re-initiates rather than rendering the loader forever.
  • The unconditional throw is removed, not returned-around. Every state now returns explicitly (isLoading→loader, isSignedIn→children, fallback, redirectTo<Navigate>, otherwise initiate sign-in and render the loader), so ProtectedRoute-Misconfiguration-001 is unreachable by construction. It never detected a misconfiguration; it was a missing return.
  • ProtectedRoute-SignInError-001 is kept but reported, not thrown. It previously lived inside an async IIFE, so it could never reach React and became an unhandled rejection. It is now constructed with the same code and origin, carries the original error as details, and goes to the package logger — matching how CallbackRoute already handles async auth failures.

2. ThunderIDProvider resumed the session twice per mount

The mount effect took isAlreadySignedIn = await client.isSignedIn(), ran resumeSession() if true, then unconditionally scheduled the refresh, re-checked isSignedIn(), and ran resumeSession() again. The pre-check can only be true when the token is valid — a state in which the re-check is also trivially true — so an already-signed-in load always paid 2 /users/me and 3 startAutoRefreshToken().

Decisions:

  • Auto-refresh is not removed; it now runs once instead of three times. startAutoRefreshToken() is still called unconditionally on every mount, before the sign-in check. Keeping it there preserves the case it exists for: when the access token expired but the refresh token is still valid, startAutoRefreshToken() refreshes immediately (timeUntilRefresh <= 0), so the isSignedIn() check that follows observes the refreshed state. A regression test covers exactly this ordering.
  • Only the redundant isSignedIn() pre-check was dropped, along with the duplicate resumeSession() it guarded. The second check is retained because it is the one that can observe a silent refresh. This also matches @thunderid/vue, which has always done this as a single if.
  • updateSession() is now single-flight. Concurrent callers share the in-flight promise. Deliberately not a cache: the ref clears on settle, so a later call (e.g. revalidation after a profile update) fetches again.

Scope decisions

  • Fixed in the SDK, not in the product. Console has 33 prop-less <ProtectedRoute> elements and Gate mounts its own provider; making the prop-less usage correct fixes both, instead of editing 33 call sites with loader/fallback and relying on convention for new routes.
  • One change was implemented, measured, and then dropped. Making signIn() await the session sync before clearing the loading state closed the window where isLoading === false while isSignedIn === false. Once ProtectedRoute no longer loops on that window, it made no difference to the request count, so it is not included. The window itself still exists and is worth a separate look.

Verification

Measured against a real console sign-in (Playwright, counting requests), building each variant separately:

Variant Fresh login Reload
Before 9, then 12 2
ProtectedRoute fix only 2 2
+ provider mount-path collapse 2 1
+ updateSession() single-flight 1 1

Final state re-measured 3× at 1/1. POST /oauth2/token unchanged (1 on login, 0 on reload). All render-phase errors gone.

Tests: 11 new for ProtectedRoute (7 fail without this change) and 5 new for ThunderIDProvider. pnpm build and pnpm format:check pass; lint findings on the touched files are unchanged or one fewer; the 2 TokenCallback failures and the nuxt vue-tsc tooling gap are pre-existing on main.

Supporting changes: re-export createPackageComponentLogger from @thunderid/react (react-router depends only on react and it was not re-exported); add @testing-library/react devDep, the missing *.tsx entries in tsconfig.spec.json, and a .vitest-attachments/ ignore to react-router — it had no component-test setup before.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented repeated sign-in attempts on protected routes during re-renders.
    • Improved handling of sign-in failures so the route continues to render while errors are logged.
    • Consolidated concurrent session refreshes into a single /users/me request.
    • Improved startup session restoration and auto-refresh behavior.
  • New Features

    • Added support for custom sign-in handling (sign-in URL navigation and callback options).
    • Exposed component logging support for React integrations.
  • Tests

    • Expanded protected-route and provider session tests, plus broader TS test discovery.
  • Chores

    • Cleaned up Vitest artifact ignores and added React Testing Library dev tooling.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PasinduYeshan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae2f8784-1487-4fe7-9c14-d79ab67d8230

📥 Commits

Reviewing files that changed from the base of the PR and between 4222022 and b13c2de.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • packages/react-router/.gitignore
  • packages/react-router/package.json
  • packages/react-router/src/components/ProtectedRoute.tsx
  • packages/react-router/src/components/__tests__/ProtectedRoute.test.tsx
  • packages/react-router/tsconfig.spec.json
  • packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx
  • packages/react/src/contexts/ThunderID/__tests__/ThunderIDProvider.test.tsx
  • packages/react/src/index.ts
📝 Walkthrough

Walkthrough

Changes

ProtectedRoute sign-in flow

Layer / File(s) Summary
Effect-driven sign-in and validation
packages/react-router/src/components/ProtectedRoute.tsx, packages/react-router/src/components/__tests__/ProtectedRoute.test.tsx, packages/react/src/index.ts, packages/react-router/package.json, packages/react-router/tsconfig.spec.json, packages/react-router/.gitignore
Sign-in is initiated once from an effect, supports redirects and custom handlers, logs failures, renders the loader during pending sign-in, and adds coverage and test tooling. createPackageComponentLogger is re-exported.

Session refresh coordination

Layer / File(s) Summary
Single-flight session updates
packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx, packages/react/src/contexts/ThunderID/__tests__/ThunderIDProvider.test.tsx
Concurrent session updates share one in-flight promise, while mount-time auto-refresh and profile restoration use the updated session path.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProtectedRoute
  participant ThunderIDRuntime
  participant Navigation
  participant Logger
  ProtectedRoute->>ThunderIDRuntime: read authentication state
  ProtectedRoute->>Navigation: navigate to signInUrl
  ProtectedRoute->>ThunderIDRuntime: initiate signIn
  ThunderIDRuntime-->>ProtectedRoute: return sign-in failure
  ProtectedRoute->>Logger: log ThunderIDRuntimeError
Loading
sequenceDiagram
  participant ThunderIDProvider
  participant ThunderIDReactClient
  participant SessionUpdate
  participant UsersMeAPI
  ThunderIDProvider->>ThunderIDReactClient: startAutoRefreshToken
  ThunderIDProvider->>ThunderIDReactClient: check isSignedIn
  ThunderIDProvider->>SessionUpdate: request updateSession
  SessionUpdate->>UsersMeAPI: fetch user profile
  ThunderIDProvider->>SessionUpdate: issue concurrent update
  SessionUpdate-->>ThunderIDProvider: reuse pending promise
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing duplicate /users/me requests during console sign-in.
Description check ✅ Passed The description covers the purpose, approach, linked issue, and verification details, with only minor template fields omitted.
Linked Issues check ✅ Passed The PR addresses #4115 by removing duplicate session-resume paths and guarding sign-in initiation, matching the reported issue.
Out of Scope Changes check ✅ Passed The extra dependency, test config, ignore rule, and logger re-export are supporting changes for the same fix and not out of scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react-router/src/components/ProtectedRoute.tsx`:
- Around line 169-228: Update the guard reset logic in the ProtectedRoute
sign-in useEffect so isLoading changes do not clear hasInitiatedSignInRef while
sign-in remains needed; reset it only when isSignedIn, fallback, or redirectTo
indicates sign-in is no longer required. In the signIn rejection handler, clear
hasInitiatedSignInRef before logging the error so a later recovery can retry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fff3a076-f714-4b99-8920-6df21515c9ad

📥 Commits

Reviewing files that changed from the base of the PR and between 0d057c0 and 016c3cf.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • packages/react-router/.gitignore
  • packages/react-router/package.json
  • packages/react-router/src/components/ProtectedRoute.tsx
  • packages/react-router/src/components/__tests__/ProtectedRoute.test.tsx
  • packages/react-router/tsconfig.spec.json
  • packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx
  • packages/react/src/contexts/ThunderID/__tests__/ThunderIDProvider.test.tsx
  • packages/react/src/index.ts

Comment thread packages/react-router/src/components/ProtectedRoute.tsx
@PasinduYeshan
PasinduYeshan force-pushed the fix/4115-duplicate-users-me branch from 4222022 to b13c2de Compare July 28, 2026 04:28
@PasinduYeshan

Copy link
Copy Markdown
Author

After the fix:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple 'GET /users/me' requests are sent when logging into the console

1 participant