Introduction

The reqwest HTTP client in Rust can fail to connect to HTTPS endpoints when TLS version negotiation fails. This happens when the server requires a specific TLS version, uses outdated cipher suites, or when the TLS backend (rustls vs native-tls) does not support the server's configuration. This is a common issue when connecting to enterprise APIs, government services, or legacy systems.

Symptoms

  • reqwest::Error: error sending request: error trying to connect: invalid certificate
  • handshake failure or protocol version errors during connection
  • Works with curl but fails with reqwest
  • Connection succeeds to some HTTPS endpoints but not others
  • Error message: tls handshake failed or unsupported protocol

Example error: `` Error: reqwest::Error { kind: Request, url: Url { "https://legacy-api.example.com/data" }, source: hyper::Error(Connect, Custom { kind: Other, error: "error:0A000086:SSL routines::certificate verify failed" }) }

Common Causes

  • Server only supports TLS 1.0/1.1 (deprecated) while client requires TLS 1.2+
  • Server uses a self-signed or internal CA certificate
  • rustls backend does not support all cipher suites that native-tls does
  • Certificate chain is incomplete on the server side
  • SNI (Server Name Indication) not properly configured

Step-by-Step Fix

  1. 1.Switch TLS backend from rustls to native-tls:
  2. 2.```toml
  3. 3.# Cargo.toml
  4. 4.[dependencies]
  5. 5.# Use native-tls (OpenSSL/Schannel) instead of rustls
  6. 6.reqwest = { version = "0.12", features = ["native-tls"], default-features = false }
  7. 7.`
  8. 8.Configure minimum TLS version:
  9. 9.```rust
  10. 10.use reqwest::Client;

// With rustls let client = Client::builder() .use_rustls_tls() .min_tls_version(reqwest::tls::Version::TLS_1_2) .build()?;

// With native-tls let connector = native_tls::TlsConnector::builder() .min_protocol_version(Some(native_tls::Protocol::Tlsv12)) .build()?;

let client = Client::builder() .use_preconfigured_tls(connector) .build()?; ```

  1. 1.Add custom certificate for internal CAs:
  2. 2.```rust
  3. 3.use reqwest::Client;
  4. 4.use std::fs;

let client = Client::builder() .add_root_certificate( reqwest::Certificate::from_pem( &fs::read("/path/to/internal-ca.pem")? )? ) .build()?;

let response = client.get("https://internal-api.example.com/data").send().await?; ```

  1. 1.Disable certificate verification (development only):
  2. 2.```rust
  3. 3.// WARNING: Only for development/testing
  4. 4.let client = Client::builder()
  5. 5..danger_accept_invalid_certs(true)
  6. 6..build()?;
  7. 7.`
  8. 8.Debug TLS negotiation with environment variables:
  9. 9.```bash
  10. 10.# For native-tls (OpenSSL)
  11. 11.SSLKEYLOGFILE=/tmp/sslkeys.log RUST_LOG=debug ./target/debug/myapp

# For rustls RUST_LOG=rustls=debug ./target/debug/myapp

# Use openssl s_client to check server TLS config echo | openssl s_client -connect api.example.com:443 -tls1_2 2>&1 | grep "Protocol|Cipher" ```

Prevention

  • Test against the actual production endpoints, not just mock servers
  • Document the TLS backend choice (rustls vs native-tls) and why
  • Use SSLKEYLOGFILE in staging to debug TLS issues with Wireshark
  • Monitor certificate expiration for all external API dependencies
  • Prefer native-tls when connecting to diverse external endpoints
  • Keep reqwest and TLS dependencies updated for security patches