Make the CLI's 403 actionable instead of a bare status code - #36
Closed
KevyVo wants to merge 3 commits into
Closed
Conversation
Add two tests for the `tier_required_pro` 403: - `test_login_403_explains_the_tier_and_still_does_not_save` extends the existing invalid-credential guard to 403. The current guard stubs 401, but the live API returns 403 for a refused credential, so that status was never exercised. Asserts the save guard still holds — non-zero exit, nothing persisted — while the message becomes actionable. - `test_tier_gated_403_on_a_normal_command_explains_itself` drives `agents list` to cover the shared error path, proving the hint reaches every command rather than only `login`, and that the machine-readable code stays in `--json`.
The API rejects some requests with the code alone, e.g.
HTTP/2 403
{"error":"tier_required_pro"}
Both `to_api_error` and `APIStatusError` assumed `body["error"]` was
always a dict wrapping a `message`. Against this body the isinstance
check failed, so the parser fell through to the generic status line and
the code was discarded — it stayed in `.body`, unread. Users saw only
"Gumloop API returned HTTP 403", and `.code` was left None.
Handle the case where `error` is a string: append it to the message and
use it as `.code`.
before: Gumloop API returned HTTP 403
after: Gumloop API returned HTTP 403: tier_required_pro
This is SDK-level, so any caller catching APIStatusError can now branch
on `.code` for these responses, not just the CLI.
A tier-gated account got "Error: Gumloop API returned HTTP 403", which says nothing about what was refused or how to resolve it. Github Issue file as: gumloop#35 Add `_ERROR_HINTS`, mapping a backend error code to plain language, and apply it in `exit_with_error`. Every command routes its errors through that function, so the hint reaches all of them rather than just `login`. When a hint exists it replaces the status line rather than appending to it — once we can name the actual problem, the raw status adds nothing for the user. The `--json` payload is unchanged apart from a new `hint` field: `message`, `code`, and `status_code` are still emitted for support and scripting. before: Error: Gumloop API returned HTTP 403 after: Error: This account needs the Pro plan or higher. Commands will keep failing until it is upgraded at https://www.gumloop.com/pricing -- or ask a workspace admin to upgrade it.
Author
|
@rbehal , @marcelo-cm , @gonzaloandresoto have a look when you get a chance 😄 , Linked to issue #35. |
3 tasks
Collaborator
|
Hey @KevyVo — thanks for digging into this. were gonna go with the server-side fix you recommended. I put up a follow-up that returns a proper PublicError from the API and teaches the SDK to read that shape, so the CLI can just print the message — no hint table needed. Appreciate you |
Author
|
@marcelo-cm was able to fix this issue by appending the fix to the backend, which a better fix. I will close this the PR infavour of #37 and close issue #35 as well. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Logging in to the CLI fails with:
This tells the user nothing: not what was refused, not why, not what to do next. Error messages should be informative and actionable.
I did not know that Gumloop need a pro level account because I had always used my work account. I only piece it together after seeing the error in my vscode debugger (dropped in CLI output) and looked at the pricing page: https://www.gumloop.com/pricing.
File as Github Issue: #35
Root cause
The server sent the reason. The client never read it.
to_api_erroronly pulled a message out of the body when the error came back wrapping one. This 403 returns the code by itself, so the parser skipped it and fell back to the generic status line — whiletier_required_prosat unread in.bodythe whole time:Confirmed against production.
Two ways to solve this
1. Annotate and wrap it client-side — what this PR does, since I don't have backend access.
2. Return a usable message from the server — the better fix.
The CLI would just print it: no lookup table to maintain, and every client benefits, not only this one. The table added here is a workaround for one endpoint, not an abstraction worth growing. Recommend doing this server-side and deleting the entry.
What the CLI looks like with my changes
Changes
The first extends the existing invalid-credential guard to 403. That guard stubs 401, but the live API returns 403 for a refused credential, so the status that actually matters was never exercised — this closes the gap while asserting the same guarantees.
The second drives
agents listto cover the shared error path, proving the fix isn't scoped tologin.Testing
Added two test:
Two tests in
tests/cli/test_login.py.test_login_403_explains_the_tier_and_still_does_not_save— asserts themessage names the tier, while exit code and credential storage are unchanged.
This extends the existing
test_login_with_invalid_credentials_...does_not_saveguard to 403; that one stubs 401, but the live API returns 403 for a refused
credential, so the status that actually matters was never covered.
test_tier_gated_403_on_a_normal_command_explains_itself— drivesagents list --jsonto prove the hint reaches the shared error path ratherthan only
login, and that the raw code is still exposed in--jsonforscripting.
Full suite passes (437),
ruffandpyrightclean.Manual verification: ran the real OAuth flow against production, captured the raw response with
curl -i, and stepped the error path in the debugger to confirm where the code was being dropped.