Error UX: friendly 403/404/429 messages + stable exit codes per failure class #134

Closed
opened 2026-06-11 00:57:41 +00:00 by stephen · 0 comments
Owner

Task

Make failure reporting scriptable on two axes: stop leaking the internal /api/v1 URL, and give failure classes stable exit codes.

  • Give 403/404/429 the same .context(...) treatment 401 already gets in ensure_success (src/client/mod.rs:573): a short human sentence ("not found: /", "forbidden: token lacks scope / no access", "rate limited, retry after N s"), keeping the raw ApiError (with the URL) as the caused by: source line main.rs:56-60 already prints, so --debug detail survives without leading with /api/v1/.
  • Map error classes to stable exit codes in run_cli (src/main.rs:54-62): downcast anyhow::Error to ApiError and return distinct codes (e.g. 3 not-found, 4 auth/forbidden, 5 rate-limit/transient), keeping 1 as the generic fallback. Document them once.
  • Prefer echoing what the user typed (owner/name, #number) over the resolved URL in the headline.

Source: rasterstate/fj#123.

Priority

p1. Distinct exit codes are the foundation of every "view the PR; if it doesn't exist, create it" agent pattern. Without them a script must capture stderr and string-match 404/couldn't be found, brittle across locales and server wording. There is no fallback that recovers a reliable failure-kind signal, which is what makes this a blocker rather than an ergonomics gap.

Reason

ensure_success forks on exactly one status: 401 gets a friendly hint, every other non-2xx is a raw ApiError dump whose Display is HTTP {status} from {url}: {message} with url the raw REST endpoint (src/client/error.rs:6-13). So a typo'd repo surfaces HTTP 404 Not Found from https://.../api/v1/repos/..., leaking plumbing the CLI otherwise hides and reading as "fj broke." Separately, main.rs maps any Err to ExitCode::from(1) (:54-62), so not-found, auth, rate-limit, and transport failures are indistinguishable. The existing 401 branch proves the maintainers already value a friendly hint; 404/403/429 just never got the same treatment.

Acceptance

  • 403/404/429 produce a short human headline; the raw ApiError (URL included) survives as the caused by: source line under --debug.
  • Stable, documented exit codes for not-found / auth-forbidden / rate-limit-transient, with 1 as the generic fallback; clap usage errors keep clap's code.
  • User-addressable failures echo the typed owner/name / #number, not the resolved /api/v1 URL.
  • Wiremock coverage in src/client/integration_tests.rs asserting headline text and exit-code class per status.
  • Exit-code contract documented (e.g. in docs/), so scripts can rely on it.
  • cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all pass.

Dependencies

None. Distinct from rasterstate/fj#125, which is about a successful command observing a failed run (this is about failed API calls). Touches the single ensure_success / main.rs error path; no parallel code path.

Size

M

## Task Make failure reporting scriptable on two axes: stop leaking the internal `/api/v1` URL, and give failure classes stable exit codes. - Give 403/404/429 the same `.context(...)` treatment 401 already gets in `ensure_success` (`src/client/mod.rs:573`): a short human sentence ("not found: <owner>/<name>", "forbidden: token lacks scope / no access", "rate limited, retry after N s"), keeping the raw `ApiError` (with the URL) as the `caused by:` source line `main.rs:56-60` already prints, so `--debug` detail survives without leading with `/api/v1/`. - Map error classes to stable exit codes in `run_cli` (`src/main.rs:54-62`): downcast `anyhow::Error` to `ApiError` and return distinct codes (e.g. 3 not-found, 4 auth/forbidden, 5 rate-limit/transient), keeping `1` as the generic fallback. Document them once. - Prefer echoing what the user typed (`owner/name`, `#number`) over the resolved URL in the headline. Source: rasterstate/fj#123. ## Priority p1. Distinct exit codes are the foundation of every "view the PR; if it doesn't exist, create it" agent pattern. Without them a script must capture stderr and string-match `404`/`couldn't be found`, brittle across locales and server wording. There is no fallback that recovers a reliable failure-kind signal, which is what makes this a blocker rather than an ergonomics gap. ## Reason `ensure_success` forks on exactly one status: `401` gets a friendly hint, every other non-2xx is a raw `ApiError` dump whose `Display` is `HTTP {status} from {url}: {message}` with `url` the raw REST endpoint (`src/client/error.rs:6-13`). So a typo'd repo surfaces `HTTP 404 Not Found from https://.../api/v1/repos/...`, leaking plumbing the CLI otherwise hides and reading as "fj broke." Separately, `main.rs` maps any `Err` to `ExitCode::from(1)` (`:54-62`), so not-found, auth, rate-limit, and transport failures are indistinguishable. The existing 401 branch proves the maintainers already value a friendly hint; 404/403/429 just never got the same treatment. ## Acceptance - [ ] 403/404/429 produce a short human headline; the raw `ApiError` (URL included) survives as the `caused by:` source line under `--debug`. - [ ] Stable, documented exit codes for not-found / auth-forbidden / rate-limit-transient, with `1` as the generic fallback; clap usage errors keep clap's code. - [ ] User-addressable failures echo the typed `owner/name` / `#number`, not the resolved `/api/v1` URL. - [ ] Wiremock coverage in `src/client/integration_tests.rs` asserting headline text and exit-code class per status. - [ ] Exit-code contract documented (e.g. in `docs/`), so scripts can rely on it. - [ ] `cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test --all` pass. ## Dependencies None. Distinct from rasterstate/fj#125, which is about a successful command observing a failed run (this is about failed API calls). Touches the single `ensure_success` / `main.rs` error path; no parallel code path. ## Size M
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rasterstate/fj#134
No description provided.