Skip to content

fix(api): propagate HTTP errors as typed envelopes + token safety + path escapes#22

Merged
dgilperez merged 1 commit into
mainfrom
fix/api-error-envelope-and-safety
May 23, 2026
Merged

fix(api): propagate HTTP errors as typed envelopes + token safety + path escapes#22
dgilperez merged 1 commit into
mainfrom
fix/api-error-envelope-and-safety

Conversation

@dgilperez
Copy link
Copy Markdown
Collaborator

@dgilperez dgilperez commented May 23, 2026

Closes the Critical bug surfaced in the post-merge review: every `print*` helper checked transport errors but not response status, so 4xx/5xx response bodies flowed through as `Envelope.Data` with no `Envelope.Error` set. Agents parsing the envelope couldn't distinguish a 404 from a real resource. The `internal/errors.ClassifyHTTPError` machinery existed for exactly this but was wired into nothing.

Critical

1. Status-code blindness — fixed across every print helper.* New `respond()` helper in `cmd/sure-cli/root/respond.go`:

  • Classifies transport failures via `errs.ClassifyNetworkError`
  • Classifies any `r.StatusCode() >= 400` via `errs.ClassifyHTTPError`
  • Always attaches `{status, body}` in error details, body capped at 1 KiB (bounds the security review's concern about echoing user-submitted data verbatim)
  • Renders the success payload via `output.Print` on the happy path

Companion `checkResponse()` for sites that do typed processing on the body (status, transactions windowing).

Migrated every print* helper (reference, investment, financial, family_exports, imports, users) and every inline GET/POST site (accounts, transactions list/show/create/update/delete, sync, whoami, plan, status, propose, export, imports preflight) — meaning every API response in the CLI now lands in the correct envelope half.

High

2/3. Token rotation safety. `client.go:74-106`, `login_cmd.go:45-73`, `refresh_cmd.go:22-39`: guard every refresh-token / access-token persistence behind an empty-string check. A partial server response would previously overwrite the saved refresh token with `""` and silently log the user out. `ensureFreshToken` also treats `>=400` from `/api/v1/auth/refresh` as a hard failure instead of persisting an empty `TokenResponse`.

4. Three missing `url.PathEscape` at `transactions_update_cmd.go:39`, `transactions_delete_cmd.go:25`, `propose_cmd.go:77` — every sibling uses it; brought these in line.

5. `tools/smoke-oauth.sh` broken/unsafe. Invoked a `--password` flag that didn't exist on `login_cmd`. Pipes the password to stdin now; `login_cmd` falls back to a plain read when stdin isn't a TTY (interactive case still uses `term.ReadPassword`).

6. `export_cmd` `--format` collision. Local `--format (csv|json)` shadowed the persistent `--format (json|table)`. Renamed local to `--out-format`. Breaking change for pre-0.2 users; README updated. This is a 0.x release.

Cleanup

  • Removed dead `--quiet` / `--trace` persistent flags (declared, read by nothing). Implementing `--trace` correctly needs a header redactor for `Authorization`/`X-Api-Key` — separate work.
  • Normalized `return` after `output.Fail` at every site touched. Defense in depth for the day `Fail` ever becomes non-exiting.

Tests

`respond_test.go` covers:

  1. Classifier contract for every status the CLI handles (401/403/404/422/429/5xx).
  2. `mergeErrorDetails` always attaches status + truncated body.
  3. Classifier-provided details preserved through the merge.

Uses httptest + the viper-wiring pattern already established in `internal/api/client_test.go`. The success path was already covered end-to-end by `dispatch_write_test.go`'s stdout-capture tests.

`go test ./... -race` green across the full suite.

Diff scope

29 files changed, +345/-250. Most of the delta is replacing 8-line `if err != nil { Fail }; if err := Print; Fail(...)` repetitions with a single `respond(r, err, res)`.

Refs the post-merge code review.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced password input handling for interactive and piped scenarios.
  • Bug Fixes

    • Fixed export command flag from --format to --out-format to prevent flag collisions.
    • Improved error handling across commands to properly stop execution on API failures.
    • Added URL escaping for transaction identifiers.
    • Strengthened token validation during login and refresh flows.
  • Chores

    • Removed deprecated --quiet and --trace flags.

…ath escapes

Closes the Critical issue surfaced in the post-merge review: every print*
helper checked transport errors but not response status, so 4xx/5xx
response bodies flowed through as Envelope.Data with no Envelope.Error
set. Agents parsing the envelope couldn't distinguish a 404 body from
the resource they asked for. The internal/errors.ClassifyHTTPError
machinery existed for exactly this but was wired into nothing.

Adds a central respond() helper in cmd/sure-cli/root/respond.go that:

- Classifies transport failures via errs.ClassifyNetworkError.
- Classifies any r.StatusCode() >= 400 via errs.ClassifyHTTPError.
- Always attaches {status, body} in error details (with the body
  capped at 1 KiB to keep large 422 payloads from spamming output and
  to bound the security review's concern about echoing user-submitted
  data verbatim in 4xx bodies).
- Renders the success payload via output.Print on the success path.

Also adds checkResponse() for call sites that do typed processing on
the body (status, transactions windowing) and need only the
fail-on-error half.

Migrates every print* helper across reference_cmds, investment_helpers,
financial_cmds, family_exports, imports, users to use respond. Inline
GET/POST sites in accounts, transactions list/show/create/update/delete,
sync, whoami, plan, status, propose, export, and imports preflight all
migrate too — meaning every API response in the CLI now lands in the
correct envelope half.

Other High-priority fixes from the review:

- **Token rotation safety** (internal/api/client.go:74-106,
  login_cmd.go:45-73, refresh_cmd.go:22-39): guard every refresh-token
  / access-token persistence behind an explicit empty-string check.
  Previously a partial server response (rotation disabled, future API
  change, 4xx body decoded into TokenResponse) would silently overwrite
  the saved refresh token with "" and log the user out. ensureFreshToken
  also now treats >=400 from /api/v1/auth/refresh as a hard failure
  rather than persisting the bogus empty TokenResponse.
- **3 missing url.PathEscape sites** (transactions_update_cmd.go:39,
  transactions_delete_cmd.go:25, propose_cmd.go:77): every sibling
  endpoint URL-escapes the id; these three did not. Brings them in
  line so an id with /, ?, #, +, : or whitespace can't break the
  request shape.
- **tools/smoke-oauth.sh:37 broken/unsafe**: invoked a --password flag
  that does not exist on login_cmd, so the script was either silently
  broken or, if anyone added the flag, would have leaked the password
  via argv/ps/shell history. Pipes the password to stdin instead.
  login_cmd now falls back to a plain stdin read when stdin is not a
  TTY (so the piped path works), keeping term.ReadPassword for the
  interactive case.
- **export_cmd --format flag collision** (export_cmd.go:25,70): local
  --format (csv|json) shadowed the persistent --format (json|table)
  from root. Renamed local flag to --out-format. Breaking change for
  pre-0.2 users; documented in README. This is a 0.x release.

Cleanup:

- **--quiet and --trace flags removed** (root.go:11-15,38-42): both
  were declared on the persistent flag set but read by nothing. Dead
  flags lie to users. Implementing --trace correctly requires a header
  redactor for Authorization / X-Api-Key (called out in the security
  review) — left as a future task with the proper plumbing.
- **Missing return after output.Fail** at every site touched (login,
  refresh, plan, transactions, etc.). Currently safe because
  output.Fail os.Exits, but a defense-in-depth fix for the day anyone
  refactors Fail to be testable / non-exiting.

Tests:

- respond_test.go covers (a) the classifier contract for every
  HTTP status the CLI handles (401/403/404/422/429/5xx), (b)
  mergeErrorDetails always attaches the status + truncated body, and
  (c) classifier-provided details are preserved. Uses httptest +
  the established viper-wiring pattern from internal/api/client_test.go.
  Avoids exercising output.Fail's os.Exit path; the success path is
  already covered by dispatch_write_test.go's stdout-capture tests.

go test ./... -race is green across the suite.

Refs the post-merge code review.
@superagent-security superagent-security Bot added the contributor:verified Contributor passed trust analysis. label May 23, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33888819-b759-4135-ae63-383a47f5bafc

📥 Commits

Reviewing files that changed from the base of the PR and between f1ef2cf and c1067d3.

📒 Files selected for processing (29)
  • README.md
  • cmd/sure-cli/root/accounts_cmd.go
  • cmd/sure-cli/root/export_cmd.go
  • cmd/sure-cli/root/family_exports_cmd.go
  • cmd/sure-cli/root/financial_cmds.go
  • cmd/sure-cli/root/imports_cmd.go
  • cmd/sure-cli/root/imports_preflight_cmd.go
  • cmd/sure-cli/root/insights_fees_cmd.go
  • cmd/sure-cli/root/insights_leaks_cmd.go
  • cmd/sure-cli/root/insights_subscriptions_cmd.go
  • cmd/sure-cli/root/investment_helpers.go
  • cmd/sure-cli/root/login_cmd.go
  • cmd/sure-cli/root/plan_cmd.go
  • cmd/sure-cli/root/propose_cmd.go
  • cmd/sure-cli/root/reference_cmds.go
  • cmd/sure-cli/root/refresh_cmd.go
  • cmd/sure-cli/root/respond.go
  • cmd/sure-cli/root/respond_test.go
  • cmd/sure-cli/root/root.go
  • cmd/sure-cli/root/status_cmd.go
  • cmd/sure-cli/root/sync_cmd.go
  • cmd/sure-cli/root/transactions_cmd.go
  • cmd/sure-cli/root/transactions_create_cmd.go
  • cmd/sure-cli/root/transactions_delete_cmd.go
  • cmd/sure-cli/root/transactions_update_cmd.go
  • cmd/sure-cli/root/users_cmd.go
  • cmd/sure-cli/root/whoami_cmd.go
  • internal/api/client.go
  • tools/smoke-oauth.sh

📝 Walkthrough

Walkthrough

This PR centralizes HTTP error handling across the CLI via a new respond helper, hardens token validation and persistence in authentication flows, adds transaction ID URL escaping, and tightens error control flow by returning early on failures rather than continuing with invalid data.

Changes

CLI Error Handling and Authentication Improvements

Layer / File(s) Summary
Centralized response/error handling infrastructure
cmd/sure-cli/root/respond.go, cmd/sure-cli/root/respond_test.go
New respond module centralizes HTTP response and error handling. respond routes transport/HTTP errors via checkResponse (which classifies and terminates), prints success results with status metadata. mergeErrorDetails truncates response bodies in error envelopes. Unit/integration tests verify HTTP classification, detail merging, and classifier preservation.
Command refactoring to centralized response handling
cmd/sure-cli/root/accounts_cmd.go, cmd/sure-cli/root/export_cmd.go, cmd/sure-cli/root/family_exports_cmd.go, cmd/sure-cli/root/financial_cmds.go, cmd/sure-cli/root/imports_cmd.go, cmd/sure-cli/root/imports_preflight_cmd.go, cmd/sure-cli/root/reference_cmds.go, cmd/sure-cli/root/sync_cmd.go, cmd/sure-cli/root/transactions_cmd.go, cmd/sure-cli/root/transactions_create_cmd.go, cmd/sure-cli/root/transactions_delete_cmd.go, cmd/sure-cli/root/transactions_update_cmd.go, cmd/sure-cli/root/users_cmd.go, cmd/sure-cli/root/whoami_cmd.go
18+ command files refactored to delegate response/error handling from inline output.Fail/output.Print to shared respond(r, err, res). Removes internal/output imports from most files, centralizing HTTP status propagation.
Error handling tightening with early returns
cmd/sure-cli/root/insights_fees_cmd.go, cmd/sure-cli/root/insights_leaks_cmd.go, cmd/sure-cli/root/insights_subscriptions_cmd.go, cmd/sure-cli/root/plan_cmd.go, cmd/sure-cli/root/export_cmd.go, cmd/sure-cli/root/propose_cmd.go, cmd/sure-cli/root/status_cmd.go
Multiple commands now return immediately after output.Fail(...) instead of continuing with potentially invalid data. Insights commands return on transaction fetch failures; plan commands return on fetch/computation/missing-account errors; export returns on request/export failures; status returns on transaction window failure.
Authentication token validation and safe persistence
cmd/sure-cli/root/login_cmd.go, cmd/sure-cli/root/refresh_cmd.go, internal/api/client.go
Login validates non-empty AccessToken and conditionally persists RefreshToken/ExpiresIn only when present/positive. Refresh command validates response access token and conditionally updates rotation fields. API client ensureFreshToken persists credentials defensively. Login also distinguishes TTY vs non-TTY stdin: hidden password for interactive terminals, plain input for piped stdin.
URL path escaping for transaction IDs
cmd/sure-cli/root/propose_cmd.go, cmd/sure-cli/root/transactions_delete_cmd.go, cmd/sure-cli/root/transactions_update_cmd.go
Adds net/url imports and uses url.PathEscape to escape transaction IDs in API request paths, preventing unescaped characters from corrupting endpoint URLs.
CLI configuration cleanup and flag updates
cmd/sure-cli/root/root.go, cmd/sure-cli/root/export_cmd.go, README.md
Removes unused --quiet and --trace persistent flags. Renames export format flag from --format to --out-format to avoid collision with root's persistent flag, updating metadata key to out_format. Updates README example to reflect new flag name.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

codex

🐰 Hops through error paths with glee,
Centralizing responses for all to see!
Tokens validated, escapes so tight,
Early returns make control flow right!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/api-error-envelope-and-safety

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@superagent-security superagent-security Bot added the pr:verified PR passed security analysis. label May 23, 2026
@dgilperez dgilperez merged commit f75c1e2 into main May 23, 2026
4 of 5 checks passed
@dgilperez dgilperez deleted the fix/api-error-envelope-and-safety branch May 23, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant