Skip to content

fix(ui): source api-keys identity from useAuthorized to stop "User ID is not set" - #30903

Merged
yuneng-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_fix_api_keys_user_id_regression
Jun 21, 2026
Merged

fix(ui): source api-keys identity from useAuthorized to stop "User ID is not set"#30903
yuneng-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_fix_api_keys_user_id_regression

Conversation

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Refs LIT-3687

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Testers on the new App Router build saw "User ID is not set" on the Virtual Keys page when landing on it directly (deep link or hard refresh). Reproduce and verify against a live proxy:

  1. Log in to the UI as normal
  2. Before the fix (current build): navigate straight to http://localhost:4000/ui/api-keys/ (or press the browser refresh while on Virtual Keys). The content area shows "User ID is not set" while the sidebar renders normally
  3. With this fix: the same deep link / hard refresh renders the Virtual Keys dashboard (key table, Create Key, teams). Refresh several times to confirm there is no flash of "User ID is not set"
  4. Click "Virtual Keys" in the sidebar from another page and confirm it still renders correctly

Why it happened: the migrated /ui/api-keys route gates rendering on useAuthorized() (a synchronous cookie decode), but the dashboard read userID from the React AuthContext, which hydrates through async effects. The route could render UserDashboard before AuthContext had set userID, tripping UserDashboard's userID == null guard. The fix reads identity from useAuthorized, the same source the route already uses to authorize, so the two can no longer disagree.

Type

🐛 Bug Fix

Changes

ApiKeysDashboard now sources userID, userRole, userEmail, accessToken, and premiumUser from useAuthorized() instead of useAuth(). useAuthorized decodes the auth cookie synchronously, so identity is available on the first render whenever the route is authorized, which removes the race against AuthContext's asynchronous hydration. useAuth is retained only for the setUserRole / setUserEmail backfill setters that UserDashboard still expects; those go away with the planned AuthContext consolidation.

The regression test mocks the exact failure condition (useAuth().userID still null while useAuthorized().userId is populated) and asserts that UserDashboard receives the populated id. It fails when identity is read from useAuth and passes when read from useAuthorized, so it pins the regression.

… is not set"

The migrated /ui/api-keys route gates rendering on useAuthorized() but read userID from the AuthContext (useAuth), which hydrates asynchronously. On a hard refresh or deep link the route could render UserDashboard before AuthContext had populated userID, so UserDashboard hit its `userID == null` guard and showed "User ID is not set". The legacy index page avoided this by gating on AuthContext's own authLoading; the migration switched the gate to useAuthorized without aligning the identity source.

Read identity from useAuthorized (a synchronous cookie decode) so userID is populated whenever the route is authorized. useAuth is kept only for the backfill setters UserDashboard still expects, until the planned AuthContext consolidation removes them.

Refs LIT-3687
@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a race condition on the /ui/api-keys route where UserDashboard would display "User ID is not set" on a hard refresh or deep link because ApiKeysDashboard was reading identity from AuthContext, which hydrates asynchronously via useEffect, before it had populated userID. The fix switches the identity source to useAuthorized(), which decodes the auth cookie synchronously via useMemo, so userID is available on the first render.

  • ApiKeysDashboard now sources userID, userRole, userEmail, accessToken, and premiumUser from useAuthorized() instead of useAuth(); only the setUserRole/setUserEmail backfill setters still come from useAuth().
  • premiumUser ?? false is added at the UserDashboard call site to satisfy its non-nullable boolean prop type, since useAuthorized() returns premiumUser: boolean | null.
  • A focused regression test mocks AuthContext.userID as null while useAuthorized returns a populated userId, asserting that UserDashboard receives the correct id — the test fails against the pre-fix code and passes after.

Confidence Score: 4/5

Safe to merge — the change is narrowly scoped to ApiKeysDashboard's identity sourcing and is backed by a regression test that directly reproduces the reported failure condition.

The fix correctly targets the async hydration race and the regression test properly pins it. Two observations worth noting: page.tsx already calls useAuthorized() and passes control to ApiKeysDashboard only once authorized, so ApiKeysDashboard invoking the hook a second time duplicates the cookie decode and useUIConfig subscription in the same render path. Separately, UserDashboard internally calls setUserRole/setUserEmail to backfill AuthContext, but since ApiKeysDashboard no longer reads those values from AuthContext, those writes no longer influence this component's rendering — the values remain consistent in practice because both sources decode the same JWT, and the PR explicitly acknowledges this as a temporary state.

No files require special attention; both changed files are straightforward and well-scoped.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx Redirects identity sourcing from AuthContext (async) to useAuthorized (sync cookie decode); adds premiumUser ?? false to satisfy the boolean prop type on UserDashboard. Creates a second invocation of useAuthorized in the same render path as page.tsx.
ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx New regression test that mocks AuthContext.userID as null while useAuthorized returns a populated userId, and asserts UserDashboard receives the correct id — correctly pins the reported failure condition.

Reviews (1): Last reviewed commit: "fix(ui): source api-keys identity from u..." | Re-trigger Greptile

Comment on lines +17 to +18
const { userId: userID, userRole, userEmail, accessToken, premiumUser } = useAuthorized();
const { setUserRole, setUserEmail } = useAuth();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Redundant useAuthorized() call in the same render path

page.tsx's ApiKeysPageContent already calls useAuthorized() and only renders ApiKeysDashboard once the hook confirms authorization. ApiKeysDashboard now calls the hook a second time in the same tree, resulting in a duplicate cookie read, two useMemo evaluations for decodeToken, and two subscriptions to useUIConfig(). Since ApiKeysPageContent already holds the full return value of useAuthorized(), the cleanest alternative is to accept the identity values as props from the parent rather than re-invoking the hook internally.

// Identity comes from useAuthorized (synchronous cookie decode) so userID is set whenever the
// route is authorized; useAuth only supplies the backfill setters UserDashboard still expects.
const { userId: userID, userRole, userEmail, accessToken, premiumUser } = useAuthorized();
const { setUserRole, setUserEmail } = useAuth();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 setUserRole/setUserEmail updates now orphaned from ApiKeysDashboard's render

UserDashboard decodes the JWT in its own useEffect and calls setUserRole/setUserEmail (lines 153 and 159 in user_dashboard.tsx) to backfill AuthContext. With the old code those AuthContext writes triggered a re-render of ApiKeysDashboard because it read userRole/userEmail from useAuth(). After this change ApiKeysDashboard reads those values from useAuthorized() (cookie-derived, immutable between renders), so the AuthContext writes from UserDashboard no longer affect this component. In practice the values are identical since both sources decode the same JWT, so behavior is unchanged. The PR comment acknowledges this is a temporary state pending AuthContext consolidation.

@yuneng-berri
yuneng-berri enabled auto-merge (squash) June 21, 2026 00:27
@yuneng-berri
yuneng-berri disabled auto-merge June 21, 2026 00:28
@yuneng-berri
yuneng-berri merged commit 8125ddd into litellm_internal_staging Jun 21, 2026
115 of 120 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_fix_api_keys_user_id_regression branch June 21, 2026 00:48
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
… is not set" (BerriAI#30903)

The migrated /ui/api-keys route gates rendering on useAuthorized() but read userID from the AuthContext (useAuth), which hydrates asynchronously. On a hard refresh or deep link the route could render UserDashboard before AuthContext had populated userID, so UserDashboard hit its `userID == null` guard and showed "User ID is not set". The legacy index page avoided this by gating on AuthContext's own authLoading; the migration switched the gate to useAuthorized without aligning the identity source.

Read identity from useAuthorized (a synchronous cookie decode) so userID is populated whenever the route is authorized. useAuth is kept only for the backfill setters UserDashboard still expects, until the planned AuthContext consolidation removes them.

Refs LIT-3687
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.

2 participants