Skip to content

fix(frontend): give API transport failures messages users can act on - #1384

Merged
IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-748
Oct 24, 2026
Merged

IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-748

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

When a proxy returned 502, the network dropped or a request timed out, users saw developer text such as "Failed to load dashboard: HTTP 502", "Failed to fetch", "Request to /api/... timed out after 90000ms" or "Unexpected token '<'", and several admin actions replaced the server's reason for a refusal with a fixed string. This PR rewrites transport, timeout, status and parse errors into plain sentences at the API client (so every existing caller benefits) and makes the listed admin actions show the server's reason.

Refs #748
Partial: the suggested error-state card with a Retry button and a <details> block for raw detail on inline load failures is not included (a per-page UI change across dashboard, plans, history and inventory), and a few direct fetch() calls are not converted yet (see Notes for review).

What changed

  • New frontend/src/api/errors.ts: friendly messages and helpers (httpStatusMessage, makeApiError, isNetworkFailure, friendlyError, plus fetchOrNetworkError/responseError/readJson for the auth calls that use fetch() directly).
  • frontend/src/api/client.ts raises friendly ApiErrors at the source: status 0 with "Can't reach CUDly. Check your connection and try again." for network failures, "CUDly took too long to respond..." for timeouts, per-status sentences when the server sent no message (5xx unavailable, 500 unexpected error, 401/403/404/429), and "CUDly sent an unexpected response..." for unparseable 2xx bodies. A specific server message still wins (and the MFA sentinel codes still work); only the backend's catch-all "Internal server error" is replaced. The raw text is kept on a new ApiError.detail field (api/types.ts), and a caller's own abort is rethrown untouched.
  • frontend/src/api/auth.ts: login, forgot/reset password and setupAdmin use the same helpers. Main (fix(frontend): sign-in, ladder save and registrations review fixes #1326) already stopped a 502 HTML page during setup from surfacing as a JSON parse error, with "Failed to create admin (HTTP 502)"; this PR replaces that with "CUDly is temporarily unavailable..." and keeps the raw status on detail.
  • users/userActions.ts, modules/registrations.ts (delete and reject), apikeys.ts (delete and revoke) and settings.ts (bulk overrides) append friendlyError(err) to their failure messages instead of dropping the reason or blaming the connection. groups/groupActions.ts deleteGroup already shows the server's reason on main (fix(frontend): hide Edit and Delete on system-managed groups #919); here it goes through friendlyError(err), which adds the friendly transport wording and replaces the empty-message fallback.
  • Tests: new api-errors.test.ts (13); api.test.ts adds network failure, caller-abort passthrough, 6 status mappings, the catch-all 500, server-message-wins and 3 setupAdmin cases (502 HTML, 400 reason kept, offline), with the old timeout/parse tests updated to the new wording. The 502 HTML case replaces main's "non-JSON error body yields Failed to create admin (HTTP 502)" test; new reason assertions in apikeys, groups, users and settings-accounts tests; new tests-e2e/friendly-api-errors.spec.ts (3).

Commits: d40cf0df (client and auth wording), 90290b8e (server reasons at the call sites, e2e spec), and the merge of main 5c6ba46e.

Why this approach

Fixing the wording once in the API client covers the roughly 30 interpolation sites listed in the issue (dashboard, history toasts, plans, inventory, API keys) without touching each one, and keeps statuses and error.details unchanged, so existing branching on err.status keeps working. Putting the server message first preserves validation reasons; treating only the exact catch-all 500 string as generic avoids hiding real reasons. Raw detail on ApiError.detail keeps support information available without showing it.

Screenshots

Screenshots were taken before this branch was merged with main; before = main at a14dc8f. After is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.

Before After
dashboard-502 before dashboard-502 after
A proxy 502 shows "Failed to load dashboard: HTTP 502". "CUDly is temporarily unavailable. Try again in a moment."
dashboard-offline before dashboard-offline after
A dropped connection shows the browser's "Failed to fetch". "Can't reach CUDly. Check your connection and try again."
apikey-revoke-reason before apikey-revoke-reason after
The server refused the revoke with a reason, but the toast (bottom right) only says "Failed to revoke API key". The toast includes the server's reason.

Verification

Run on this branch's head 5c6ba46e, from frontend/:

  • Merged with main at a229b81; conflicts in frontend/src/api/auth.ts, frontend/src/__tests__/api.test.ts, frontend/src/groups/groupActions.ts and frontend/src/settings.ts resolved, see the merge commit.
  • npm run lint: 0 errors (existing warnings only). Typecheck and build pass.
  • Jest: 3899 of 3900 passed, 1 skipped. Before the merge, --randomize on the api, api-errors, apikeys, groups and settings-accounts suites: 360/360. users.test.ts fails 9 tests under one random seed with and without this branch's test changes (existing order dependence in the group-toggle/bulk-handler tests).
  • Playwright (e2e-local.sh): 205 passed, including the new friendly-api-errors.spec.ts (502 HTML page, dropped connection, HTML page served with 200).
  • Secrets scan clean (planted key flagged); scripts/test-git-secrets-allowlist.sh: 137 PASS.

Notes for review

  • Done: transport, timeout, HTTP-status and parse wording in the API client and the direct-fetch auth calls; server reasons at the specific call sites the issue lists (users, groups, registrations x2, API keys x2, bulk overrides).
  • Left out: the error-state card with Retry and a <details> block for raw detail on inline load failures; per-site rewording of the ~15 history.ts toasts (they get the friendly text automatically but keep their "Failed to X:" prefixes); other direct fetch() calls (getResetTokenStatus, checkAdminExists, getPublicInfo, deployment info, commitmentOptions).
  • Copy to check: the five messages in api/errors.ts, all phrased around "CUDly" (e.g. "CUDly is temporarily unavailable. Try again in a moment."). 401 says "Your session has expired. Sign in again.", which applies only when the server sends no message.
  • Main's start-up error card (fix(frontend): show an error state when init() fails, not a silent Home #1338, init-error.ts) expects the old HTTP <status> message, so with this branch a failed init reads "Details: HTTP 502: CUDly is temporarily unavailable. Try again in a moment." Showing err.detail there would be cleaner; not changed here.
  • Behavior change: for a timeout, err.message no longer contains the endpoint and milliseconds (moved to err.detail), and a parse failure is now an ApiError instead of a SyntaxError. No source code depended on either.

Overlaps

Note for the UI owner

This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.

🤖 Generated with Claude Code

IAmJSD and others added 2 commits October 23, 2026 23:44
The API client built its errors from developer text: a bare "HTTP 502"
when the server sent no message, "Request to /api/x timed out after
90000ms" on timeout, the browser's "Failed to fetch" when the network
dropped, and "Unexpected token '<'" when a proxy answered a 2xx with an
HTML page. Dozens of toasts and inline errors interpolate err.message, so
users saw that text with no next step. Login, password reset and
first-admin setup call fetch() directly and had the same problems;
setupAdmin parsed a 502 HTML body with no catch.

The new api/errors.ts maps each case to a sentence (can't reach CUDly,
took too long, temporarily unavailable, unexpected response, plus 401,
403, 404 and 429 wording). The client uses it at the source, so every
existing caller benefits without changes, and the direct-fetch auth
calls use the same helpers. A specific server message still wins (and
the MFA sentinel codes still work); only the backend's catch-all
"Internal server error" is replaced. The raw text stays on the new
ApiError.detail field for support, statuses are unchanged (0 for
transport failures), and a caller's own abort is rethrown untouched.

Refs LeanerCloud#748

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting a user, group, API key or registration, revoking an API key,
rejecting a registration, and saving bulk overrides all replaced the
server's error with a fixed string ("Failed to delete user", "Check
your connection and try again"), so an admin never learned why the
action was refused. These now append friendlyError(err): the server's
reason when it sent one, otherwise the transport wording.

Also adds a Playwright spec for the transport wording on the dashboard:
a 502 HTML page, a dropped connection and an HTML page served with 200.

Refs LeanerCloud#748

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm effort/l Weeks triaged Item has been triaged urgency/this-quarter Within the quarter impact/all-users Affects every user type/bug Defect labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 16 billable files and costs up to $4.00.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between a229b81 and 5c6ba46.

Files selected for processing: 16
  • frontend/src/__tests__/api-errors.test.ts
  • frontend/src/__tests__/api.test.ts
  • frontend/src/__tests__/apikeys.test.ts
  • frontend/src/__tests__/groups.test.ts
  • frontend/src/__tests__/settings-accounts.test.ts
  • frontend/src/__tests__/users.test.ts
  • frontend/src/api/auth.ts
  • frontend/src/api/client.ts
  • frontend/src/api/errors.ts
  • frontend/src/api/types.ts
  • frontend/src/apikeys.ts
  • frontend/src/groups/groupActions.ts
  • frontend/src/modules/registrations.ts
  • frontend/src/settings.ts
  • frontend/src/users/userActions.ts
  • frontend/tests-e2e/friendly-api-errors.spec.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Conflicts:
- frontend/src/api/auth.ts (setupAdmin): kept this branch's
  responseError(). It covers main's fix (LeanerCloud#1326: a non-JSON 502 body no
  longer surfaces as a SyntaxError, a JSON error still wins) and also
  gives 5xx the friendly status sentence and keeps the raw HTTP status in
  ApiError.detail. Main's "Failed to create admin (HTTP 502)" text is
  therefore replaced by "CUDly is temporarily unavailable...".
- frontend/src/__tests__/api.test.ts (setupAdmin): kept this branch's
  502/400/offline tests, main's "JSON error body still surfaces the server
  message" test and main's new base64Encode suite. Dropped main's
  "non-JSON error body yields Failed to create admin (HTTP 502)" test: the
  same scenario is asserted by this branch's friendly-502 test.
- frontend/src/groups/groupActions.ts: imports from both sides
  (friendlyError and main's systemGroups); deleteGroup's catch keeps main's
  intent (show the server's reason, LeanerCloud#919) through this branch's
  friendlyError(), which also words transport failures and an empty
  message instead of falling back to a bare "Failed to delete group".
- frontend/src/settings.ts: imports from both sides (friendlyError and
  main's settings-account-delete).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE 5c6ba46ec5acf0cbb9d6dc7627b01521382086d7

Blocking:

  • none
    Non-blocking:
  • frontend/src/init-error.ts:20: main's fix(frontend): show an error state when init() fails, not a silent Home #1338 describeError expects the old HTTP ${status} message. With this branch a failed init reads "Details: HTTP 502: CUDly is temporarily unavailable. Try again in a moment.", and the message !== HTTP ${status} guard now never matches. Show err.detail there instead.
  • frontend/src/tests/groups-system-managed-919.test.ts:102: "falls back to the generic message when the error has no text" passes only via toContain('Failed to delete group'). The toast is now "...: Something went wrong. Try again in a moment.", so the name is stale and the assertion loose. Pin the new text.
  • frontend/src/modules/registrations.ts:236,264: no test covers the new server-reason text on delete or reject failure (registrations.test.ts has no rejected-mock case).
  • frontend/src/api/auth.ts:440: main's httpError() for getPublicInfo/getDeploymentInfo is a second error builder next to makeApiError, with the developer wording this PR removes elsewhere. It only reaches console.warn today, so the direct-fetch follow-up can handle it.
    Attacked: origin/main...HEAD (merge-base a229b81) is all branch work. The 5 newer main commits touch none of these files and add none of the old error strings. Ran the merge through show --cc and checked it against both parents. auth.ts is main plus the branch, with fix(frontend): sign-in, ladder save and registrations review fixes #1326's getPublicInfo/httpError kept. client.ts keeps main's "unparseable 2xx must reject", now wrapped. groupActions.ts keeps fix(frontend): hide Edit and Delete on system-managed groups #919's server reason via friendlyError plus the systemGroups import. settings.ts had an imports-only conflict. In api.test.ts only main's "Failed to create admin (HTTP 502)" case was dropped, and the branch's friendly-502 test covers the same non-JSON path. Grepped for callers that depend on "HTTP n", timed out, TypeError/SyntaxError or message matching, and for unescaped err.message in innerHTML: none. Checked caller-abort passthrough, the MFA sentinel via responseError, and results scope in bulk overrides. Jest (api, api-errors, apikeys, groups, groups-919, users, settings-accounts, registrations, init-error, auth): 703/703.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 5c6ba46e merged onto current main (15e021db; merge-tree e1e9ca30; Linux, Node, headless Chromium):

  • This branch's own checks passed at this head: lint, typecheck, build, the full jest suite, the full Playwright e2e suite, and the git-secrets checks (see Verification in the PR body).
  • The combined tree of every UI PR in this merge wave (tree dc090cc2, main plus 17 PRs) passes npm ci, lint, typecheck, build, jest (1 skipped, 4097 passed, 4098 total), and the full Playwright suite (232 passed).
  • CI is green on 5c6ba46e (CI Success, Run pre-commit hooks).
  • The independent review above approves this exact head.
  • Nothing was applied to any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 24, 2026 19:27
@IAmJSD
IAmJSD merged commit 6bd0850 into LeanerCloud:main Oct 24, 2026
29 checks passed
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at 5c6ba46e.

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

Labels

effort/l Weeks impact/all-users Affects every user priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant