Skip to content

Show API error messages as text instead of raw JSON - #260

Open
sanieldalib wants to merge 1 commit into
mainfrom
salib/readable-api-errors
Open

sanieldalib wants to merge 1 commit into
mainfrom
salib/readable-api-errors

Conversation

@sanieldalib

@sanieldalib sanieldalib commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

🚀 What

  • A failed API call now prints its message as text instead of the raw JSON body.

The API no longer appends pydantic's For further information visit https://errors.pydantic.dev/… line to validation messages (a server-side change), so the "after" examples below show the message as the API now returns it. The "before" example is the body as it was captured.

Before and after

Captured from main and this branch, built locally, running baseten grid route list against a local server that replays each response body. The 400 body is a real validation error from the API.

Multiline message (HTTP 400)

Before:

list routes: baseten API error (HTTP 400): {"code": "VALIDATION_ERROR", "message": "1 validation error for APIKeysRequestV1\ntype\n  Input should be 'PERSONAL', 'GRID', 'WORKSPACE_MANAGE_ALL', 'WORKSPACE_EXPORT_METRICS', 'WORKSPACE_INVOKE' or 'WORKSPACE_MANAGE_API_KEYS' [type=enum, input_value='ROUTES', input_type=str]\n    For further information visit https://errors.pydantic.dev/2.12/v/enum"}

After:

list routes: request failed (HTTP 400, VALIDATION_ERROR)

  1 validation error for APIKeysRequestV1
  type
    Input should be 'PERSONAL', 'GRID', 'WORKSPACE_MANAGE_ALL', 'WORKSPACE_EXPORT_METRICS', 'WORKSPACE_INVOKE' or 'WORKSPACE_MANAGE_API_KEYS' [type=enum, input_value='ROUTES', input_type=str]

One-line message (HTTP 403)

Before: list routes: baseten API error (HTTP 403): {"code": "PERMISSION_DENIED", "message": "Authorization error"}
After:  list routes: Authorization error (HTTP 403, PERMISSION_DENIED)

Proxy body with only error (HTTP 401)

Before: list routes: baseten API error (HTTP 401): {"error": "Unauthorized"}
After:  list routes: Unauthorized (HTTP 401)

--verbose (after only)

list routes: request failed (HTTP 400, VALIDATION_ERROR)

  1 validation error for APIKeysRequestV1
  type
    Input should be 'PERSONAL', 'GRID', 'WORKSPACE_MANAGE_ALL', 'WORKSPACE_EXPORT_METRICS', 'WORKSPACE_INVOKE' or 'WORKSPACE_MANAGE_API_KEYS' [type=enum, input_value='ROUTES', input_type=str]

  response body: {"code": "VALIDATION_ERROR", "message": "1 validation error for APIKeysRequestV1\ntype\n  Input should be 'PERSONAL', 'GRID', 'WORKSPACE_MANAGE_ALL', 'WORKSPACE_EXPORT_METRICS', 'WORKSPACE_INVOKE' or 'WORKSPACE_MANAGE_API_KEYS' [type=enum, input_value='ROUTES', input_type=str]"}

Live API, 404 (after only)

$ baseten grid route describe --id does-not-exist
describe route does-not-exist: No CodeRoute matches the given query. (HTTP 404, NOT_FOUND)
  • A one-line message stays on one line.
  • --verbose adds the raw response body below the message.
  • Unchanged: --output json still carries the raw body in message, plus api_status_code, api_error_code and api_details. Exit codes are unchanged. A body with no readable message (HTML from a proxy, for example) still prints as before.

💻 How

  • apiErrorText in internal/cmd/errors.go finds the API response in the error chain, reads the message from message, or from error or detail when those are strings (inference API and proxies), and keeps whatever the command wrapped around it (listing Grid API keys: ).
  • The framework uses it where it prints a failed command's error to stderr. The JSON envelope is built from the unformatted error, so its message keeps the raw body; the JSONError.Message doc comment no longer claims it is identical to stderr.
  • Covers the management, inference and sandbox generated clients. Not covered: the high-level sandbox client's sandbox.APIError, which is already decoded and prints its own format.

🔬 Testing

  • New tests in errors_test.go replay that 400 body through org billing usage and assert the exact stderr text, plus the one-line, proxy (error only), --verbose, JSON and non-JSON-body cases.
  • Existing TestJSONError* tests pass unchanged.
  • go test ./... passes except Test_Grid_Setup_RealHarness and Test_Grid_ClaudeServedModel_RealClaudeCode, which fail the same way on unmodified main on my machine (they drive locally installed harnesses).

🤖 Generated with Claude Code

@sanieldalib
sanieldalib force-pushed the salib/readable-api-errors branch 2 times, most recently from 908fde7 to c81924d Compare November 13, 2026 21:48
@sanieldalib
sanieldalib changed the base branch from main to salib/grid-overwrite-warning November 13, 2026 21:48
Base automatically changed from salib/grid-overwrite-warning to main November 13, 2026 22:17
A failed API call printed the response body verbatim, so a multiline
message showed up as one long line of JSON with escaped newlines. The
status, code and message now print as text: one line for a one-line
message, and the message indented on its own lines otherwise. --verbose
adds the raw body, and JSON output still carries it in the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sanieldalib
sanieldalib force-pushed the salib/readable-api-errors branch from c81924d to 05849ad Compare November 13, 2026 22:20
@sanieldalib
sanieldalib requested a review from cretz November 13, 2026 22:27
@sanieldalib
sanieldalib marked this pull request as ready for review November 13, 2026 22:27
Copilot AI balanced review requested due to automatic review settings November 13, 2026 22:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T22:28:24.558393Z 05849ad Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Decoded JSON messages and error codes can emit unsanitized terminal controls to stderr, allowing screen clearing or overwritten diagnostics; sanitization and an escaped-ANSI regression test are needed.

1 open issue
What changed in this PR

This PR makes API failures easier to read in the CLI by displaying decoded messages instead of raw JSON, while preserving JSON output and exit codes.

Changes:

  • Formats single-line and multiline API messages with HTTP status, error code, and command context.
  • Includes the raw response body in verbose output and preserves fallback behavior for unreadable bodies.
  • Adds formatting tests and clarifies the JSON error message documentation.
File Description
internal/​cmd/​errors.go Extracts API responses and formats readable error messages.
internal/​cmd/​errors_test.go Tests message formatting, verbose output, JSON preservation, and fallback behavior.
internal/​cmd/​command.go Applies readable formatting when printing command failures to stderr.
cmd/​errors.go Clarifies that JSON error messages retain the raw response body.

Review effort: Balanced

Comment thread internal/cmd/errors.go
Comment on lines +178 to +184
message = strings.TrimSpace(message)
if message == "" {
return "", false
}
statusText := fmt.Sprintf("HTTP %d", status)
if code := cmp.Or(body.Code, body.ErrorCode); code != "" {
statusText += ", " + code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants