feat(cube-cli): suggest updating the CLI when a request fails on the API - #11592
Conversation
API errors are frequently caused by an outdated CLI talking to a newer
API, so the error output now suggests updating the CLI as a possible fix.
- add a shared `ApiError` (with status, url and response body) plus an
`isApiError` marker-based type guard
- throw it from `CubeCloudClient` requests and from `cubejs typegen`
instead of a bare `Error`, and include the response body in the message
- pass the caught error into `displayError` from every command so the
update suggestion is printed under "Need some help?" for API errors
- fix `cubejs typegen` crashing inside its own catch block when the
failure isn't shaped like `{ error: { error } }` (e.g. network errors)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jv8Ykk5wWXab2KjCsK58vi
This reverts commit b2cddfe.
A CLI that lags the API is a common cause of otherwise puzzling API errors, so point at `cube update` before the user starts digging. - add `ApiError`, a marker error type for failures that came from an API response (unsuccessful status, or a body the CLI can't make sense of), and `is_api_error` to spot it anywhere in an error chain - return it from the API client and from the OAuth device/refresh flows; transport failures (DNS, TLS, connection refused) stay plain errors, since an update won't fix those - print a hint under the error when the failure is an API error, pointing at the update notice instead of repeating it when one was just shown - honor `CUBE_NO_UPDATE_CHECK` for the hint too, and document it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jv8Ykk5wWXab2KjCsK58vi
|
Claude finished @paveltiunov's task in 2m 15s —— View job Review: solid design, one UX issue worth fixing before merge — 2 medium, 2 lowFull review (4 inline comments posted)
What's goodThe marker-error approach is the right shape for this. A few things done well:
Findings
#1 is the one I'd want addressed. 401/403/404 are the most common API failures a user hits, and none of them are fixed by updating. Attaching the hint there means it mostly fires where it's wrong, which teaches people to skip it in the Security / performanceNothing of concern. Not verifiedI wasn't able to run |
Telling someone already on the latest release to update is noise that teaches them to ignore the hint, so consult the background release check before offering it. - turn the check's result into an `UpdateCheck` outcome (newer / up to date / disabled / undetermined) instead of a pre-rendered notice, and resolve it once for both the notice and the hint - print no hint when the CLI is current or checks are opted out; name the available release when it is behind; suggest looking when the check could not complete - color the hint on a terminal, matching the notice it sits under, and keep it plain in piped output - cover the policy with tests that don't touch env or terminal state Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jv8Ykk5wWXab2KjCsK58vi
|
Claude finished @paveltiunov's task in 1m 40s —— View job Re-review of
|
| # | Severity | Location | Issue | Status |
|---|---|---|---|---|
| 1 | Medium | client.rs:199-205 |
401/403/404 still get a cube update hint that contradicts the fix the error prescribes |
prior thread, still open |
| 5 | Medium | main.rs:302-305 |
resolve is now awaited unconditionally — non-TTY runs block up to 1.5 s on the GitHub check for a result they discard |
new, inline |
#1 is unchanged — client.rs wasn't touched by this commit, so the existing thread still applies as written. It's worth re-reading in light of UpToDate: the new gating means the hint now fires less often overall, but on a genuinely stale CLI a 401 still prints "session expired — run cube login" immediately followed by "run cube update", and only one of those is the fix.
#5 is a side effect of the refactor. Previously the 1500 ms timeout lived inside print_notice, behind its is_terminal early return, so piped runs never waited. Now resolve is awaited before either consumer is consulted — see the inline comment for the one-line guard.
Minor
- Neither the notice nor the hint honours
NO_COLOR;owo_colorsemits escapes whenever theis_terminalbranch is taken. Pre-existing crate-wide behaviour, not introduced here — mentioning only because this commit adds a second colored call site. checkisNoneforUpdate/Completion, which maps toUnknown(main.rs:304) rather than a distinct "not checked". Harmless today since neither command produces anApiError, so theUnknownhint can't surface from that path.
Not verified
cargo test / cargo clippy still require an approval this environment doesn't grant, so this remains a static review. The five new update.rs tests read as correct against the code, and the color test in particular pins the behaviour that would otherwise be easy to regress. If you'd like these run here, adding Bash(cargo test) / Bash(cargo clippy) to --allowedTools would do it.
Resolving the check moved ahead of both its consumers, so every non-interactive run paid up to 1.5s for an answer it then discarded: the notice is interactive-only, and the hint only runs on an API failure. A piped successful command took 1526ms against an unresponsive release endpoint, against 28ms with checks opted out. Await the handle only when something will read it — 24ms on that same path now, with the notice and all four hint outcomes unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jv8Ykk5wWXab2KjCsK58vi
|
Claude finished @paveltiunov's task in 1m 43s —— View job Re-review of
|
| # | Severity | Location | Issue | Status |
|---|---|---|---|---|
| 1 | Medium | client.rs:205 |
401/403/404 classified as API errors | prior thread — author declined, see below |
| 6 | Low | main.rs:302 |
error: output is delayed by the up-to-1.5 s wait on a failing run |
new, inline |
#1 — your reply stands on its own: gating the hint on UpdateCheck::Newer means a current CLI gets a plain 401, so the contradiction only surfaces for someone who is genuinely stale and hitting an expired session, where mentioning both is defensible. "Any API error" being the product requirement settles it. Leaving the thread unresolved since it's a judgement call you made rather than a defect that disappeared, but I'm not asking for a change.
#6 is small and mostly pre-existing — resolve sits above eprintln!("error: ..."), so when the release check hasn't landed the user waits for GitHub before seeing why their command failed. This commit widens the exposure (piped failing runs now await too, where before this PR they never did). Trivially avoidable by printing the error first; the cost is that the notice then lands below the error instead of above it, so it's your call on layout.
Minor
Unknownnow means three different things: "check failed", "check timed out", and "we never asked". Harmless while the only two consumers areprint_notice(TTY-gated) andapi_error_hint(error-gated) — the skip condition is exactly the negation of those two — but a third consumer added later would get theUnknownhint text on a path where no check ran. ANotCheckedvariant would make that unrepresentable if it ever comes up.- Dropping the un-awaited
JoinHandledetaches the task rather than aborting it; the process exits immediately after, so the in-flight request dies with the runtime. No leak. NO_COLORstill isn't honoured (crate-wide, pre-existing) — unchanged from the last round.
Not verified
cargo test / cargo clippy remain unavailable here — both invocations were declined by the permission layer, so this is again a static review. Adding Bash(cargo test) / Bash(cargo clippy) to --allowedTools would let me actually run them. Statically, resolve's new Option<JoinHandle<UpdateCheck>> signature matches the single call site at main.rs:302 and check's type at main.rs:279, and wanted is exercised by the new test so it won't trip dead_code.
Only the hint needs the check's answer, so a slow or blocked GitHub sat between the user and the reason their command failed — 1521ms of silence against an unresponsive release endpoint, now 20ms. The notice moves below the error line on a failing run as a result, which also puts it directly above the hint that refers back to it. Successful runs are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jv8Ykk5wWXab2KjCsK58vi
|
Claude finished @paveltiunov's task in 1m 6s —— View job Re-review of
|
| # | Severity | Location | Issue | Status |
|---|---|---|---|---|
| 1 | Medium | client.rs:205 |
401/403/404 classified as API errors | prior thread — author declined, not re-raised |
No new findings. Re-affirmed the one prior thread still applies as written (client.rs is untouched since it was opened); leaving it unresolved because it's your judgement call, not a defect that disappeared. The four earlier threads are resolved.
Minor (not worth a change here)
- On a network where
api.github.comis blackholed but the Cube API is reachable — a locked-down CI is the realistic case — every API failure still pays the 1.5 s wait and then prints theUnknownvariant ("a newer release may already fix it"). That's the feature's inherent cost on a path that has already failed, and it's now paid after the error is visible, which is the part that mattered. NO_COLORstill isn't honoured (crate-wide, pre-existing) — unchanged, and you've flagged it as a separate change.
Not verified
cargo test / cargo clippy were declined by the permission layer again (three invocations, all blocked), so this remains a static review. Adding Bash(cargo test) / Bash(cargo clippy) to --allowedTools would let me actually run them. The commit touches no test-covered logic — main.rs has no tests, and the eight update.rs tests exercise wanted/hint_for, neither of which changed.
Check List
Description of Changes Made
A CLI that lags the API is a common cause of otherwise puzzling API errors, so
cubenow points atcube updatebefore the user starts digging — but only when this binary really is behind:The hint reuses the background release check that already powers the "new release available" notice, so it costs no extra request:
cube updateto check"CUBE_NO_UPDATE_CHECK=1How it works:
src/error.rswithApiError, a marker error type for failures that came from an API response (an unsuccessful status, or a body the CLI can't make sense of), plusis_api_errorto spot it anywhere in ananyhowerror chain.api_bail!mirrorsbail!at those sites, so the call sites read as they did before.client.rsreturns it for every non-success status and for the "web app HTML instead of JSON" case;oauth.rsfor the device-authorization, token-poll and refresh endpoints, including responses it can't parse.not logged in,API URL is empty) stay plain — an update won't fix those, so they get no hint.spawn_checknow yields anUpdateCheckoutcome (Newer/UpToDate/Disabled/Unknown) instead of a pre-rendered notice string, resolved once and shared by the notice and the hint. On a terminal, when the notice has just announced the release, the hint refers back to it rather than repeating the version and command.api.github.comneither delays scripted runs nor sits between the user and their error.owo_colors::Style— colored on a terminal, plain in piped output, since unlike the notice the hint also prints non-interactively (a stale pinned CLI in CI is where the advice pays off).Tests
cargo test(33 passed),cargo clippy --all-targets -- -D warnings, andcargo fmt --all --checkall clean inrust/cube-cli. Tests coveris_api_error(direct, wrapped in context, negative cases), every mapped status plus the HTML case in the client, all four hint outcomes, the color split, and the "don't wait when nobody's listening" predicate — none of them touching env or terminal state.Behaviour was also verified by running the built binary against a local server returning 400, with fake release endpoints pinned to a newer and an equal version, a socket that accepts and never answers, a closed port, and an unconfigured context — under both a pipe and a pty. Two latency measurements from that harness, against the unresponsive release endpoint: