Skip to content

feat(cli): print the API's client error message - #1214

Merged
trunk-io[bot] merged 4 commits into
mainfrom
tyler/client-error-codes
Oct 1, 2026
Merged

trunk-io[bot] merged 4 commits into
mainfrom
tyler/client-error-codes

Conversation

@TylerJang27

@TylerJang27 TylerJang27 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Overview

A 4xx from the Trunk API can now carry {"code": "<CODE>", "message": "<text>"} (trunk-io/trunk#34193). The CLI prints message in the error report as given, beneath the generic "There was an unexpected error…" line.

The server owns the wording, and the CLI keeps no per-code text. So a code added after this release still reaches the users of this release. That's the property a codes-only design can't have: an "upgrade your CLI" code would never reach the old clients it's for.

The first codes are:

  • TEST_COLLECTION_REQUIRED: an organization on test collections uploaded without --test-collection-id.
  • TEST_COLLECTION_NOT_FOUND: the id matches no collection in the organization. Today that's a 404, which the CLI reports as a bad token.
  • UNSUPPORTED_CLI_VERSION: declared on the server; nothing sends it yet.
Details
  • message.rs: ClientErrorBody { code: Option<String>, message: Option<String> }.
  • client.rs: status_code_help becomes async and takes its options as a StatusCodeHelp struct. On a 4xx it reads the body. If the body has a non-blank code or message, it wraps the reqwest error in a ClientErrorExplanation { code, message }.
    • A message over 2,048 characters is truncated on a character boundary with "…".
    • A code without a message is printed as "The Trunk API rejected this request with error code ."
  • error_report.rs: ErrorReport::output prints the message beneath the base message.
    • It has to be a typed error rather than plain context text. Callers add an ApiErrorEndpoint context on top, and the report prints only the base message, so plain text would sit in the chain unseen.
    • It's found with anyhow's downcast_ref, which reaches context values at any depth. chain() yields anyhow's context wrapper instead of the value.
  • code is kept on the error, so a later change could branch on it, for example to make a code fatal. Nothing branches on it yet.

Considerations

  • The reqwest error stays underneath. The retry policy (AbortableRetry) and is_unauthorized both find it through chain()/root_cause(), so retries and exit codes are unchanged. A 400 still isn't retried, and still falls back to the test exit code.
  • A body with neither a code nor a message is ignored, so the error is unchanged from today. That covers non-JSON, an empty body, blank fields, and older servers.
  • The message is server-controlled text printed to CI logs. public-api only sends messages from a declared table (CLIENT_ERROR_MESSAGES in trunk1), never gRPC details. The truncation is a guard against a malformed body, not a trust boundary.
  • Not changed here: the hints status_code_help already builds are still never shown. Those are UNAUTHORIZED_CONTEXT, NOT_FOUND_CONTEXT and the per-call messages. On any 4xx the function returns reqwest's error instead. That's a separate fix.

Testing

  • New call_api.rs tests:
    • A body with a message carries it (with its code), keeps a 400 reqwest error as the root cause, and isn't retried.
    • A code with no message, or a blank one, is named in the printed error.
    • An overlong message is truncated.
    • A body with neither field, a plain Bad Request body and an empty body each leave the error unchanged.
  • New error_report.rs test: the message is printed beneath the base message even under an endpoint context.
  • cargo test -p api -p trunk-analytics-cli: all pass except three swift_test_xunit upload tests. Those fail identically on main in my environment (no Swift toolchain).
  • cargo clippy --all-targets --all-features --no-deps: no warnings on changed lines. cargo fmt is clean.

🤖 Generated with Claude Code

A 4xx from the Trunk API can carry {"code": "<CODE>"}. The CLI maps
known codes to its own explanation and prints it in the error report;
the first is TEST_COLLECTION_REQUIRED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

😎 Merged successfully - details.

@codecov-commenter

codecov-commenter commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.85057% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.08%. Comparing base (209c229) to head (8c35f9c).

Files with missing lines Patch % Lines
api/src/client.rs 96.82% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1214      +/-   ##
==========================================
+ Coverage   83.74%   84.08%   +0.33%     
==========================================
  Files          74       74              
  Lines       17745    17884     +139     
==========================================
+ Hits        14860    15037     +177     
+ Misses       2885     2847      -38     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trunk-staging-io

trunk-staging-io Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
pending_quarantine_test should be quarantined when run with variant A test marked as pending was expected to fail but unexpectedly passed. Logs ↗︎
variant_quarantine_test should be quarantined when run with variant A test expected the sum of 2 + 2 to be 5, but it was actually 4, indicating a failed assertion. Logs ↗︎

View Full Report ↗︎ ⋅ Docs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

The API now sends a message with each client error code, so the CLI
keeps no wording of its own and a code added after this release still
reaches its users.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@TylerJang27 TylerJang27 changed the title feat(cli): explain API client error codes feat(cli): print the API's client error message Sep 30, 2026
Comment thread api/src/client.rs Outdated
Comment on lines +428 to +430
if message.is_empty() || message.len() > MAX_CLIENT_ERROR_MESSAGE_LEN {
return None;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like we shouldn't return None for these reasons. I think code without message is still good signal? And also, maybe we should just slice the message to the max len?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, done in 8c35f9c. code and message are both optional now, and the body is ignored only when it has neither. A code without a message prints "The Trunk API rejected this request with error code ." A message over 2,048 characters is truncated on a character boundary with a trailing "…" instead of being dropped. New tests cover code-only, whitespace-only message, and truncation.

Comment thread api/src/client.rs
})
}

pub(crate) async fn status_code_help<T: FnMut(&Response) -> String>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we update this fn to accept a struct as an agrument? This is too many args IMO

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 8c35f9c: status_code_help(response, StatusCodeHelp { check_unauthorized, check_not_found, create_error_message, api_host, org_url_slug }). The response stays positional, and all four callers (three in client.rs, one test) use the struct.

Comment thread api/src/call_api.rs Outdated
Comment on lines +535 to +536
r#"{"code":"SOME_CODE"}"#,
r#"{"code":"SOME_CODE","message":" "}"#,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit, optional: use the serde_json::json macro for these and the json string above

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 8c35f9c: the test bodies are built with json!.

status_code_help takes its options as a StatusCodeHelp struct.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@trunk-io
trunk-io Bot merged commit be7bb49 into main Oct 1, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants