seanmonstar/reqwest · error · std::io::Error

HTTP/3 only supports 'https' or 'h3' schemes, got

Error message

HTTP/3 only supports 'https' or 'h3' schemes, got: {}

What it means

Thrown by extract_domain() in the HTTP/3 connection pool (src/async_impl/h3_client/pool.rs:364-385) when a request routed to the h3 client carries a URI whose scheme is neither 'https' nor 'h3'. HTTP/3 runs over QUIC, which requires TLS 1.3, so plaintext 'http' URIs are fundamentally invalid for the h3 transport. The guard fires before any connection is opened and the offending scheme string is interpolated into the message.

Solutions

  1. Ensure the request URL uses the 'https://' scheme when forcing HTTP/3: replace 'http://host' with 'https://host' before calling .version(http::Version::HTTP_3).
  2. Remove the forced .version(http::Version::HTTP_3) and let reqwest negotiate the protocol; only the server's ALPN-advertised h3 over QUIC will actually use HTTP/3.
  3. If you must use a custom scheme internally, register an 'h3' URI scheme and pass an 'h3://host' URL, since pool.rs:369 explicitly accepts 'h3'.
  4. Add a runtime guard that asserts url.scheme() == "https" before constructing an HTTP/3 request so the failure surfaces at call site.

Example fix

// before
let resp = client
    .get("http://api.example.com/data")
    .version(http::Version::HTTP_3)
    .send()
    .await?;

// after
let resp = client
    .get("https://api.example.com/data")
    .version(http::Version::HTTP_3)
    .send()
    .await?;
Defensive patterns

Strategy: validation

Validate before calling

fn assert_h3_compatible(url: &url::Url) -> Result<(), &'static str> {
    match url.scheme() {
        "https" | "h3" => Ok(()),
        other => Err("HTTP/3 requires an https:// or h3:// URL"),
    }
}

// call before .version(http::Version::HTTP_3)
let url = url::Url::parse(&raw_url)?;
assert_h3_compatible(&url)?;

Type guard

fn is_h3_scheme(url: &url::Url) -> bool {
    matches!(url.scheme(), "https" | "h3")
}

Try / catch

// reqwest::Error has no public kind() for this; match on the Display source.
if let Err(e) = resp {
    if e.to_string().contains("HTTP/3 only supports") {
        // downgrade to HTTP/2/1.1 over TLS and retry
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Building a Client with the 'http3' feature and forcing .version(http::Version::HTTP_3) on a RequestBuilder pointed at an 'http://' URL. Also occurs when HttpVersionPref::Http3 / .http3_prior_config() is set but the request URL is plaintext http. The check at pool.rs:369 compares scheme.as_str() strictly to 'https' or 'h3', so any other scheme (http, ws, ftp) on a request dispatched to the h3 client triggers it.

Common situations: Mixing .version(http::Version::HTTP_3) with a non-TLS endpoint during local testing against 'http://localhost'. Misconfigured base URLs in env vars (HTTP where HTTPS is required). A load balancer terminating TLS and forwarding plain http URLs while the client is pinned to HTTP/3. Feature-flag mismatch: enabling 'http3' but targeting servers that only serve HTTP/1.1 or h2 cleartext.

Related errors


AI-assisted analysis of seanmonstar/reqwest@9f06fd28ab (2026-08-10). Data as JSON: /api/errors/23cfb8016bdd1fc9. Report an issue: GitHub.

Appendix: source

Thrown at src/async_impl/h3_client/pool.rs:370

        }
    }

    fn size_hint(&self) -> hyper::body::SizeHint {
        if let Some(content_length) = self.content_length {
            hyper::body::SizeHint::with_exact(content_length)
        } else {
            hyper::body::SizeHint::default()
        }
    }
}

pub(crate) fn extract_domain(uri: &mut Uri) -> Result<Key, Error> {
    let uri_clone = uri.clone();
    match (uri_clone.scheme(), uri_clone.authority()) {
        (Some(scheme), Some(auth)) => {
            let scheme_str = scheme.as_str();
            if scheme_str != "https" && scheme_str != "h3" {
                return Err(Error::new(
                    Kind::Request,
                    Some(Box::new(std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        format!(
                            "HTTP/3 only supports 'https' or 'h3' schemes, got: {}",
                            scheme_str
                        ),
                    ))),
                ));
            }
            Ok((scheme.clone(), auth.clone()))
        }
        _ => Err(Error::new(Kind::Request, None::<Error>)),
    }
}

pub(crate) fn domain_as_uri((scheme, auth): Key) -> Uri {
    http::uri::Builder::new()

View on GitHub (pinned to 9f06fd28ab)