Skip to content

fix(ui): re-apply 401/403 error.type taxonomy on upstream v1.87.0 networking.tsx - #68

Merged
songkuan-zheng merged 1 commit into
ship/v1.87.0from
fix/v1.87.0-ui-401-redirect-relayered
Jun 5, 2026
Merged

fix(ui): re-apply 401/403 error.type taxonomy on upstream v1.87.0 networking.tsx#68
songkuan-zheng merged 1 commit into
ship/v1.87.0from
fix/v1.87.0-ui-401-redirect-relayered

Conversation

@songkuan-zheng

Copy link
Copy Markdown
Collaborator

Tier classification

  • A - [x] B internal UX - [ ] C - [ ] D

Summary

Re-applies the v1.87.0 bump's Wave 7 `networking.tsx` 401/403 taxonomy
that was rolled back by PR #62 (which reset `networking.tsx` to upstream
verbatim to fix a broken Docker build introduced by Wave 7's misuse of
`git checkout --theirs`).

Backend half of the contract (`_classify_auth_failure` in
`auth_exception_handler.py`, emits `error.type` field in 401/403 bodies)
landed via Wave 6a (PR #56) and is already in production. This PR makes
the frontend read that field and route accordingly.

Implementation

Built on top of upstream-clean `networking.tsx` — no cherry-pick. Manual
layering of the original 3 commits' intent:

  • module-level `AUTH_ERROR_TYPE_TO_ACTION` lookup table mapping backend
    `error.type` (`auth_session_expired` / `auth_invalid_credentials` /
    `auth_permission_denied` / `token_not_found_in_db` / fallback) to UI
    action (`REDIRECT_LOGIN` / `TOAST` / `HEURISTIC`)
  • `SESSION_EXPIRED_SIGNALS` substring fallback for when backend omits
    `error.type` (legacy clients)
  • `extractErrorType()` reads both `{error:{type}}` and top-level
    `{type}` shapes
  • `triggerSessionExpiredRedirect()` single source of truth for
    clear-cookies + redirect
  • new `handleErrorResponse(response, errorData)` — preferred
    status-aware entry point (uses HTTP status to decide redirect vs
    inline)
  • existing `handleError(errorData)` extended to also read `error.type`
    first, then fall back to heuristic — auto-benefits ~163 legacy
    callsites + the 17 hook files

`callMCPTool` callsite hoisted `let errorData: any = null` above the
`try` block (gotcha caught from the original #38 Patch 2).

Conflict resolutions

N/A — manual re-layer, no cherry-pick conflicts.

What changes

  • `ui/litellm-dashboard/src/components/networking.tsx`: +191 lines net
    (10183 → 10374). Preserves all upstream additions (Memory CRUD,
    `tagListCall(accessToken, startTime, endTime)`, etc).
  • `ui/litellm-dashboard/src/components/networking.test.ts`: +356 lines
    net (455 → 811). +38 new tests across 3 new describe blocks.

Verification

\`\`\`
$ cd ui/litellm-dashboard && npm run build
✓ Compiled successfully in 26.2s
37 static pages generated cleanly

$ npx vitest run src/components/networking.test.ts
Test Files 1 passed (1)
Tests 45 passed (45)
\`\`\`

Behavior change

Before this PR After
Session expired (`auth_session_expired`) redirect to `/login` redirect ✓
Bad key (`auth_invalid_credentials`) redirect to `/login` redirect ✓
403 permission denied (`auth_permission_denied`) redirect to `/login` ❌ inline error, stay on page ✓
`token_not_found_in_db` 401 redirect to `/login` (no reason shown) redirect ✓ with intent
Heuristic fallback (no `error.type`) redirect on `Authentication Error` substring unchanged

Type

🐛 Bug Fix

…working.tsx

Wave 7 of the v1.87.0 bump originally added structured auth error
handling to ui/litellm-dashboard/src/components/networking.tsx via
three commits:

  - be8a895  fix(ui): route 401/403 by backend error.type, heuristic only as fallback (#30)
  - f73ae47  fix(ui): redirect to login on session-expired 401, not on all 401s
  - 0383afb  fix(ui): redirect to login on 401 token_not_found_in_db (#38)

PR #61 dropped them with a careless `git checkout --theirs` during the
v1.87.0 cherry-pick. PRs #62 and #63 reset networking.tsx +
networking.test.ts to upstream verbatim to unblock the Docker build.
This PR re-layers that work on top of the upstream-clean baseline.

The matching backend change (`_classify_auth_failure` in
litellm/proxy/auth/auth_exception_handler.py, Wave 6a) is already in
production. It emits `error.type` with one of:

  - auth_session_expired       → REDIRECT_LOGIN
  - auth_invalid_credentials   → REDIRECT_LOGIN
  - token_not_found_in_db      → REDIRECT_LOGIN (legacy specific type)
  - expired_key                → REDIRECT_LOGIN (legacy specific type)
  - auth_permission_denied     → TOAST (the original bug)
  - *_model_access_denied      → TOAST
  - team_member_permission_error → TOAST
  - budget_exceeded            → TOAST
  - auth_error                 → HEURISTIC (legacy marker fallback)

Three layers added to networking.tsx:

1. AUTH_ERROR_TYPE_TO_ACTION dispatch table + extractErrorType helper.
2. Status-aware handleErrorResponse(response, errorData) — preferred
   when caller has the Response object. Three-tier decision:
   structured type → status+cookie heuristic → delegate to legacy.
3. Legacy handleError(errorData) now also reads error.type FIRST.
   This was the live-e2e bug fix (commit 2 of #30): the ~30 existing
   fetch sites call handleError directly, not handleErrorResponse, so
   making the legacy entry point smart auto-propagates the D1 taxonomy
   contract without touching call sites.

Callsite migration (commit 3 of #38, Patch 1): 163 fetch sites in
networking.tsx were calling handleError(errorMessage) — pre-stringifying
the body before handleError could read error.type. Bulk-replaced with
handleError(errorData) so the structured-type path actually fires in
production. The MCP tool call path has a special errorData scope
(declared inside `try`), so hoist `let errorData: any = null` outside
the try and pass `errorData ?? responseText` to handleError.

Tests: 38 new test cases across three describe blocks:
  - handleErrorResponse - status-aware auth handling (5 tests)
  - handleErrorResponse - type-based auth routing (D1 contract) (14 tests)
  - handleError (legacy) - now also reads error.type (8 tests)
  - existing networking - expired session handling (3 tests, unchanged)
  Total: 45 tests pass (was 7).

Verification:
- npm run build → ✓ Compiled successfully in 26.2s, 37 static pages
- npx vitest run src/components/networking.test.ts → 45/45 pass

Tier: C (universal bug fix — auth error UX correctness)
Tried upstream first? No — this is companion code to our backend
_classify_auth_failure (Wave 6a) which is itself an internal Tier D
mechanism. The structured-type contract is the carry; upstream may
adopt a similar taxonomy independently.

Conflict resolutions: N/A (clean re-application on upstream-clean
baseline, no `git cherry-pick` was used — manual port of the
three source commits' intent onto upstream's current networking.tsx
structure).
@songkuan-zheng
songkuan-zheng merged commit 2548dcc into ship/v1.87.0 Jun 5, 2026
@songkuan-zheng
songkuan-zheng deleted the fix/v1.87.0-ui-401-redirect-relayered branch June 5, 2026 01:49
songkuan-zheng added a commit that referenced this pull request Jun 5, 2026
…#69)

## Case 24 (anthropic_beta_overrides_bedrock_gateway) — register

The runbook landed in Wave 6d (commit 99f6ff1) as a .md file only —
no data fixture, no README index entry, no case_NN function in the
runner. After this commit, case 24 is recorded in the index as
Tier=real and `case_24()` in the runner SKIPs unconditionally with a
clear "Tier=real — requires Bedrock" message.

Rationale: the behaviour case 24 verifies (Bedrock validation of the
older tool-search-tool flag, after `anthropic_beta_overrides` rewrites
the auto-injected advanced-tool-use header) cannot be driven by the
mock — Bedrock's beta-flag validator is the whole point. Keep the
manual runbook around for human / Claude execution against real
Bedrock; skip in automation so the e2e summary still records it.

## Case 25 (backend auth error.type contract) — new

Locks the wire contract that PR #68's frontend 401 redirect taxonomy
depends on: every 401/403 response carries a structured `error.type`
drawn from `ProxyErrorTypes`. Wave 6a added `_classify_auth_failure`
and the UI reads its output to pick redirect vs inline error. Without
an e2e gate a future upstream auth refactor could silently route
exceptions around the classifier; unit tests on the classifier itself
would still pass while the field disappears from the wire.

The fixture runs four probes (one per ProxyErrorTypes value) against
the running proxy:

  A1 no Authorization header        → auth_invalid_credentials
  A2 malformed key (no sk- prefix)  → auth_invalid_credentials
  A3 sk- key absent from DB         → token_not_found_in_db
  A4 internal_user role + admin route → auth_permission_denied

`auth_session_expired` is intentionally left to unit tests in
test_auth_exception_handler.py — driving it deterministically would
require a `duration:"1s"` key + a sleep, which is fragile under load.

## Verification

```
e2e/tools/run-all-cases --mock-only
→ Total: 24  Pass: 16  Fail: 0  Skip: 8
  PASS  25 auth-error-type-contract: PASS: all 4 auth error.type contract probes hit expected values
  SKIP  24 anthropic-beta-overrides-bedrock-gateway (Tier=real — requires Bedrock)
```

Tier: B (internal e2e infra).
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.

1 participant