Skip to content

Cloud handlers: return a concrete Error instead of Box<dyn Error> to drop the exit-code downcast #233

@sdairs

Description

@sdairs

Follow-up from PR #222.

Background

PR #222 added gh-style exit codes. CloudError now carries a kind (Auth / Generic) so 401/403 and missing-creds map to exit 4. But cloud command handlers (cloud/commands.rs, cloud/postgres.rs) and run_postgres return Box<dyn std::error::Error>, which type-erases the CloudError. To recover kind at the top level we currently downcast:

fn boxed_cloud_error_to_top_level(e: Box<dyn std::error::Error>) -> Error {
    match e.downcast::<cloud::CloudError>() {
        Ok(ce)     => cloud_error_to_top_level(*ce),    // kind recovered -> exit 4 possible
        Err(other) => Error::Cloud(other.to_string()),  // not a CloudError -> exit 1
    }
}

Problem

The downcast only succeeds if the boxed concrete type is exactly CloudError. If any handler stringifies a CloudError before boxing — e.g. client.foo().await.map_err(|e| format!("...: {e}"))? instead of plain ? — the downcast misses and exit 4 silently degrades to exit 1. It still compiles, the message still prints, the process still exits non-zero; only the machine-readable auth signal is lost. Unit tests pin the router contract, but nothing structurally prevents a future handler from breaking it.

Box<dyn Error> was chosen for ergonomic ? across heterogeneous error sources (CloudError, serde_json, uuid, chrono, io, plain validation strings) — that convenience is exactly what erases the type we now need to inspect.

Proposed improvement

Have cloud handlers return a concrete, matchable error type instead of Box<dyn Error> — e.g. the top-level Error (or a dedicated cloud error enum) with #[from] impls for the parse/io/cloud sources. Then kind is carried as Error::AuthRequired vs Error::Cloud from the point of failure, main matches on it directly, and boxed_cloud_error_to_top_level + the downcast disappear entirely.

Scope / cost

Touches the signature and error construction of every cloud handler in cloud/commands.rs and cloud/postgres.rs, plus run_postgres. Larger than #222, hence split out. No user-facing behavior change — purely making the exit-code routing structurally correct rather than downcast-dependent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrustPull requests that update rust code

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions