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 certificatehandshake failureorprotocol versionerrors during connection- Works with
curlbut fails withreqwest - Connection succeeds to some HTTPS endpoints but not others
- Error message:
tls handshake failedorunsupported 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.Switch TLS backend from rustls to native-tls:
- 2.```toml
- 3.# Cargo.toml
- 4.[dependencies]
- 5.# Use native-tls (OpenSSL/Schannel) instead of rustls
- 6.reqwest = { version = "0.12", features = ["native-tls"], default-features = false }
- 7.
` - 8.Configure minimum TLS version:
- 9.```rust
- 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.Add custom certificate for internal CAs:
- 2.```rust
- 3.use reqwest::Client;
- 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.Disable certificate verification (development only):
- 2.```rust
- 3.// WARNING: Only for development/testing
- 4.let client = Client::builder()
- 5..danger_accept_invalid_certs(true)
- 6..build()?;
- 7.
` - 8.Debug TLS negotiation with environment variables:
- 9.```bash
- 10.# For native-tls (OpenSSL)
- 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
SSLKEYLOGFILEin 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
reqwestand TLS dependencies updated for security patches