Skip to content

Connect errors lose their cause, so cert failures are retried and messages say only "client error (Connect)" #116

Open

Description

@liquidsec

Problem

When a pooled request fails while connecting, dispatch_request in src/client/hyper.rs builds the error from hyper-util's top-level error only:

let msg = format!("request failed: {}", e);
let err_str = e.to_string().to_lowercase();
if err_str.contains("ssl") || err_str.contains("tls") || err_str.contains("certificate") {
    ClientError::tls(msg)
} else {
    ClientError::connection(msg)
}

hyper-util's Display for its client error is just client error ({kind:?}). The real cause (certificate verify failed, connection refused, proxy rejected CONNECT, and so on) is in source(), which is never read. Two things follow:

  1. The message doesn't say why. Every connect failure reads request failed: client error (Connect), whether the cert was bad, the port was closed, or a proxy refused the tunnel.
  2. TLS and certificate failures are retried. The check for "ssl", "tls" or "certificate" never matches, so they're classified as ErrorKind::Connection, which is_retryable retries. With the default retries=1, every host with a bad cert costs an extra attempt plus at least retry_wait_min_ms of backoff.

This happens on direct requests too, not just through a proxy.

Reproduction

verify_certs=True against the self-signed server in tests/tls_integration.rs (test_verify_certs_true_rejects_self_signed) fails with:

ClientError { message: "request failed: client error (Connect)", kind: Connection }

The test only asserts is_err(), so it passes anyway.

Proposed Solution

Walk the error's source() chain, append the causes to the message, and run the TLS check against the full chain, so a cert failure is ErrorKind::Tls and isn't retried. Tighten test_verify_certs_true_rejects_self_signed to assert ErrorKind::Tls.

The error text gets longer as a result, so anything matching on the exact string client error (Connect) would need to change.

Activity

  1. added theissue type on Oct 3, 2026
  2. liquidsec commented on Oct 26, 2026

    @liquidsec
    ContributorAuthor

    This is fixed on the 1.0 branch (#128), which hasn't merged yet.

    • TLS failures: the connector now records why a handshake failed, and the client looks that up instead of relying on hyper-util's top-level error. The error comes back as a TLS error with the actual reason, for example "certificate verification failed: ..." or "no cipher suite in common". TLS errors aren't retried, so a bad cert no longer costs an extra attempt.
    • Failures before TLS (020335b): a refused connection, a DNS failure or a proxy refusing the tunnel now carries the cause from further down the error chain, for example tcp connect error: Connection refused (os error 111). A connect timeout is now reported as a timeout instead of a retryable connection error, which matches what the direct path already did. The same commit also removes the old substring check from dispatch_direct, which could never match.

    I'll leave this open until #128 merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions