Skip to content

fix(client): render the error envelope's sentence in inline warnings - #308

Merged
eddietejeda merged 1 commit into
mainfrom
fix/attach-warning-unwraps-error-envelope
Sep 19, 2026
Merged

eddietejeda merged 1 commit into
mainfrom
fix/attach-warning-unwraps-error-envelope

Conversation

@eddietejeda

@eddietejeda eddietejeda commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

ApiError::message formatted the raw response body, so every warning that folds an API error inline printed the JSON envelope instead of the sentence inside it. create --attach showed:

warning: could not attach 'attach-src': 409 Conflict: {"error":{"message":"this catalog is scoped to another database and cannot be attached here","code":"CONFLICT"}}

and — as review pointed out — the query preview fallback did the same for a failed full-result fetch (could not fetch full result (404 Not Found: {"error":…})), which the first version of this PR missed.

Fixed at the root rather than at one call site: message now runs the body through the same unwrapping the fatal path (format_fail_message) already does, via a shared server_explanation, so the inline and the fatal rendering of one response cannot drift apart. The status stays in the line so a non-JSON body still says what happened. Every message caller gets the fix at once; nothing has to remember to reach for a second accessor. The only string-consuming caller, cross_source_hint, matches text that lives inside error.message, so it is unaffected.

Not included, deliberately: the 403 ACCESS_DENIED token-scope hint stays on the fatal path only. It is a multi-line hint, and message is interpolated into single-line parentheticals.

Noticed while working runtimedb#1382 / runtimedb#1412, which is where this error text comes from.

Test plan

  • api_error_message_unwraps_the_error_envelope409 Conflict: this catalog is scoped …, no JSON.
  • api_error_message_and_fail_message_agree_on_an_empty_body — pins the two paths to the same base text.
  • a_stream_that_cannot_open_falls_back_to_the_preview now mocks a real envelope and asserts the warning quotes the sentence, not {"error" — so it guards query.rs:355, which it did not before.
  • Dropped the earlier mockito test that drove a pure function through the full attach HTTP path; it would have broken on unrelated SDK schema changes.
  • cargo test — 528 unit + all integration targets pass; cargo clippy --all-targets -- -D warnings and cargo fmt --check clean.

@eddietejeda
eddietejeda requested a review from a team as a code owner September 18, 2026 21:59
@eddietejeda
eddietejeda requested review from shefeek-jinnah and removed request for a team September 18, 2026 21:59
Comment thread src/client/sdk.rs Outdated
///
/// Unlike [`print`](Self::print) it never probes the stored credential, so
/// it needs no live `Api` and adds no auth or token-scope hint.
pub fn user_message(&self) -> String {

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.

nit: two other message() callers are also user-facing, not string matchers (not blocking).

src/commands/query.rs:355 builds could not fetch full result ({}) and passes it to note_preview, which prints the note above the preview rows. src/commands/query.rs:1730 asserts the same string. Both still render the raw envelope, so a persisted-result fetch that returns {"error":{"message":"…"}} shows the JSON body inline.

Switch query.rs:355 to user_message() in this PR, or drop the claim from the PR description that the remaining callers are matchers or tests.

Comment thread src/client/sdk.rs Outdated
Comment on lines +302 to +304
ApiError::Status { status, body } if body.trim().is_empty() => {
format!("HTTP {status} (empty response body)")
}

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.

nit: the empty-body arm has no test (not blocking).

This arm diverges from util::api_error, which returns "unexpected empty response from server" for a blank body (src/util.rs:446). user_message returns "HTTP {status} (empty response body)" instead. The divergence is deliberate and matches format_fail_message, but nothing pins it, so a later refactor that deletes the arm keeps every test green.

Add a case to api_error_message_formats_status_and_transport in the same module, or a sibling test asserting the status reaches the string for a 502 with an empty body.

claude[bot]
claude Bot previously approved these changes Sep 18, 2026
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

`ApiError::message` formatted the raw response body, so every warning
that folded an API error inline printed the JSON envelope instead of
the sentence inside it. `create --attach` showed it as

    warning: could not attach 'src': 409 Conflict: {"error":{"message":
    "this catalog is scoped to another database…","code":"CONFLICT"}}

and the query preview fallback did the same for a failed full-result
fetch ("could not fetch full result (404 Not Found: {"error":…})").

Fix it where it lives: `message` now runs the body through the same
unwrapping the fatal path already does, via a shared
`server_explanation`, so the inline and the fatal rendering of one
response cannot drift. The status stays in the line so a non-JSON body
still says what happened. Every `message` caller gets the fix at once;
nothing has to remember to pick a second accessor.
@eddietejeda
eddietejeda force-pushed the fix/attach-warning-unwraps-error-envelope branch from 2970763 to 6dbbeef Compare September 19, 2026 00:32
@eddietejeda eddietejeda changed the title fix(databases): unwrap the error envelope in the create --attach warning fix(client): render the error envelope's sentence in inline warnings Sep 19, 2026
Comment thread src/client/sdk.rs
/// `format_fail_message` (fatal) build on it.
fn server_explanation(status: reqwest::StatusCode, body: &str) -> String {
if body.trim().is_empty() {
format!("HTTP {status} (empty response body)")

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.

nit: a bodyless response prints the status twice in inline warnings (not blocking).

Fix: return only empty response body here, and let format_fail_message build error: HTTP {status} (empty response body) from the status it already has.

ApiError::message prefixes {status}: and this branch prefixes HTTP {status} again. A bodyless 502 reaches the user through src/commands/databases.rs:1981 as warning: could not attach 'src': 502 Bad Gateway: HTTP 502 Bad Gateway (empty response body). The assertion at src/client/sdk.rs:1141 pins that duplication.

Comment thread src/client/sdk.rs
/// The exit path ([`format_fail_message`]) shares that base and adds the
/// credential-probe hints on top, which need a live `Api` this does not
/// have.
pub fn message(&self) -> String {

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.

nit: the PR description describes a different change (not blocking).

Update the description before merge.

The description says the PR adds ApiError::user_message and leaves message() alone. This commit changes message() itself, which is a wider change: every caller now sees the unwrapped text, including fail_query and attach_connection. The description also lists a test named failed_attach_reports_the_servers_sentence_not_its_envelope, which the diff does not contain. The description becomes the squash-merge commit body, so the permanent record names a method and a test that do not exist.

@eddietejeda
eddietejeda merged commit fb6ed24 into main Sep 19, 2026
14 checks passed
@eddietejeda
eddietejeda deleted the fix/attach-warning-unwraps-error-envelope branch September 19, 2026 16:11
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.

1 participant