#3094·reqwest

"Invalid Server Name" when requesting an IPv6 literal through a proxy

Author: kp-patrick-arnsCreated Aug 26, 2026Updated Aug 26, 2026

An HTTPS request to an IPv6 literal fails with "Invalid Server Name" when a proxy is configured. The same request to an IPv4 literal, or to a hostname, works.

Example code to reproduce this issue:

rust
use tokio::io::{AsyncReadExt, AsyncWriteExt};

// A CONNECT proxy that accepts the tunnel and holds the socket open.
async fn proxy() -> std::net::SocketAddr {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();

    tokio::spawn(async move {
        while let Ok((mut sock, _)) = listener.accept().await {
            let mut buf = [0u8; 1024];
            let _ = sock.read(&mut buf).await;
            let _ = sock
                .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
                .await;

            tokio::spawn(async move {
                let mut sink = [0u8; 1024];
                while sock.read(&mut sink).await.unwrap_or(0) != 0 {}
            });
        }
    });

    addr
}

#[tokio::main]
async fn main() {
    let proxy = proxy().await;
    let client = reqwest::Client::builder()
        .proxy(reqwest::Proxy::https(format!("http://{proxy}")).unwrap())
        .timeout(std::time::Duration::from_secs(2))
        .build()
        .unwrap();

    for url in ["https://[2001:db8::1]/", "https://192.0.2.1/"] {
        println!("{url}\n  {:?}\n", client.get(url).send().await.unwrap_err());
    }
}

[dependencies]
reqwest = { version = "0.13.4", features = ["rustls"] }
tokio = { version = "1", features = ["full"] }

https://[2001:db8::1]/ -> reqwest::Error { kind: Request, url: "https://[2001:db8::1]/", source: hyper_util::client::legacy::Error(Connect, "Invalid Server Name") }

https://192.0.2.1/ -> reqwest::Error { kind: Request, url: "https://192.0.2.1/", source: TimedOut }

The IPv4 request reaches the TLS handshake and times out against the dummy proxy, which is expected. The IPv6 request never gets that far.

Why: ServerName is built from dst.host() in the two proxy paths in src/connect.rs, and for an IPv6 literal that returns the bracketed form a URI requires. ServerName::try_from rejects it. The direct path is unaffected because HttpsConnector strips the brackets. hyper-rustls fixed the same issue in rustls/hyper-rustls#181