Skip to content

Map reqwest errors to semantically correct Twirp error codes#314

Merged
tclem merged 3 commits intomainfrom
tclem/fix-reqwest-error-mapping
Apr 10, 2026
Merged

Map reqwest errors to semantically correct Twirp error codes#314
tclem merged 3 commits intomainfrom
tclem/fix-reqwest-error-mapping

Conversation

@tclem
Copy link
Copy Markdown
Member

@tclem tclem commented Apr 10, 2026

The From<reqwest::Error> impl was mapping all reqwest errors to InvalidArgument, which is wrong for transport failures like connection refused, DNS resolution failures, and timeouts. Downstream consumers (e.g. retry logic) couldn't distinguish transient infrastructure failures from actual bad input.

Approach

Replace the blanket mapping with variant-specific logic:

  • is_builder()InvalidArgument — actually malformed input (bad URL, etc.)
  • is_redirect() / is_body() / is_decode()Internal — unexpected server behavior or response parsing failure
  • everything else (connect, timeout, request) → Unavailable — transient and retryable

The default is Unavailable rather than Internal because it's safer to assume a transport error is transient and retryable.

The .with_rust_error(e) pattern is preserved so the original reqwest error is available for debugging.

Generated via Copilot (Claude Opus 4.6) on behalf of @tclem

Replace the blanket From<reqwest::Error> → InvalidArgument mapping with
variant-specific logic:

- is_builder() → InvalidArgument (actually bad input)
- is_redirect() / is_body() / is_decode() → Internal (unexpected behavior)
- everything else (connect, timeout, request) → Unavailable (transient)

This lets downstream consumers distinguish transport failures from
application errors, enabling correct retry decisions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread crates/twirp/src/error.rs Fixed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tclem tclem marked this pull request as ready for review April 10, 2026 19:40
@tclem tclem requested a review from a team as a code owner April 10, 2026 19:40
Copilot AI review requested due to automatic review settings April 10, 2026 19:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Twirp’s From<reqwest::Error> conversion to map reqwest error variants to more semantically appropriate Twirp error codes so downstream consumers can distinguish bad input from transient transport failures.

Changes:

  • Replace blanket InvalidArgument mapping for reqwest::Error with variant-based mapping (InvalidArgument / Internal / Unavailable).
  • Preserve .with_rust_error(e) to retain underlying error details for debugging.
  • Add tests covering timeout and request-builder reqwest errors.
Show a summary per file
File Description
crates/twirp/src/error.rs Refines reqwest→Twirp error-code mapping and adds targeted tests for the new behavior.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment thread crates/twirp/src/error.rs
Comment thread crates/twirp/src/error.rs Outdated
Comment thread crates/twirp/src/error.rs Outdated
- Use localhost TcpListener instead of non-routable IP for timeout test
- Fix misleading comment on builder error test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@jorendorff jorendorff left a comment

Choose a reason for hiding this comment

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

Back when we started all this, should have made the generated trait generic, like

pub trait HaberdasherApi<E>: Send + Sync {
    async fn make_hat(
        &self,
        req: twirp::Request<MakeHatRequest>,
    ) -> Result<twirp::Response<MakeHatResponse>, E>;
    ...
}

Clients would be HaberdasherApi<twirp::ClientError> and servers would be HaberdasherApi<TwirpErrorResponse>.

The ClientError could straightforwardly say what it means, no need to pretend twirp is the world's only source of errors:

enum ClientError {
    Io(std::io::Error),
    BadResponseBody(serde_json::Error),
    // ... timeout, etc.
    Twirp(TwirpErrorResponse),
}

Using the same error type for both, like we do now, would also be bad if we have a Twirp service that talks to another backend Twirp service; someone might naively ? that error up the stack. Then the backend service's 404s become our Rust service's 404s, its 429s become our 429s; and any error meta data it gives us gets forwarded to our user.

But as you said in chat, maybe it's too late to redesign now.

Please bump the 0.x version number (this is an API breaking change) and republish.

@jorendorff
Copy link
Copy Markdown
Contributor

i just hope we don't get into a situation later where we need to distinguish a real decode error from a twirp 500, or a connect from a timeout.

@tclem
Copy link
Copy Markdown
Member Author

tclem commented Apr 10, 2026

Yeah, what do you think. Should we really break things and refactor properly? Maybe as a follow up?

@jorendorff
Copy link
Copy Markdown
Contributor

Yeah, what do you think. Should we really break things and refactor properly? Maybe as a follow up?

I might give it a shot after this.

@tclem tclem added this pull request to the merge queue Apr 10, 2026
Merged via the queue into main with commit 6fa2c7d Apr 10, 2026
5 checks passed
@tclem tclem deleted the tclem/fix-reqwest-error-mapping branch April 10, 2026 22:06
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.

4 participants