From 1c25ac442c608ef611f6c723a47046eeca782618 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 20:16:43 +0000 Subject: [PATCH] fix: never send HTTP/3 requests past the proxy reqwest sends HTTP/3 requests through its own QUIC connector, which ignores proxies. With `with_http3()` and a proxy, the first request to a host went through the proxy, and once the host advertised h3 (Alt-Svc or its HTTPS DNS record), later requests went out over QUIC from the machine's own IP address. HTTP/3 is now disabled (with a warning) when a proxy is set, either with `with_proxy` or in `HTTPS_PROXY` / `ALL_PROXY`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0148RYR4BTGyCYHENSRZjC3S --- impit/src/errors.rs | 2 +- impit/src/impit.rs | 50 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/impit/src/errors.rs b/impit/src/errors.rs index d815590b..97d01570 100644 --- a/impit/src/errors.rs +++ b/impit/src/errors.rs @@ -84,7 +84,7 @@ pub enum ImpitError { UrlMissingHostnameError(String), #[error("The URL uses an unsupported protocol (`{0}`). Currently, only HTTP and HTTPS are supported.")] UrlProtocolError(String), - #[error("The request was made with http3_prior_knowledge, but HTTP/3 usage wasn't enabled.")] + #[error("The request was made with http3_prior_knowledge, but HTTP/3 usage wasn't enabled (or was disabled because a proxy is set).")] Http3Disabled, #[error("The request method `{0}` is invalid. Only GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS and TRACE are supported.")] InvalidMethod(String), diff --git a/impit/src/impit.rs b/impit/src/impit.rs index 4609ccef..3ea72941 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -1,6 +1,6 @@ use tokio::sync::RwLock; -use log::debug; +use log::{debug, warn}; use reqwest::{cookie::CookieStore, header::HeaderMap, Method, Response, Version}; use std::{fmt::Debug, net::IpAddr, sync::Arc, time::Duration}; use url::Url; @@ -72,7 +72,6 @@ pub enum RedirectBehavior { /// .with_ignore_tls_errors(true) /// .with_proxy("http://localhost:8080".to_string()) /// .with_default_timeout(Duration::from_secs(10)) -/// .with_http3() /// .build() /// .unwrap(); /// @@ -175,6 +174,10 @@ impl ImpitBuilder { /// To enforce HTTP/3 usage, use the `http3_prior_knowledge` option in the `RequestOptions` struct when /// making the request. /// + /// HTTP/3 is never used with a proxy, set either with [`with_proxy`](Self::with_proxy) or in the + /// `HTTPS_PROXY` or `ALL_PROXY` environment variables: proxies only carry TCP, so QUIC requests would + /// skip the proxy and go out from this machine's IP address. With a proxy, the client uses HTTP/2. + /// /// Note that this feature is experimental and may not work as expected with all servers. pub fn with_http3(mut self) -> Self { self.max_http_version = Version::HTTP_3; @@ -226,6 +229,15 @@ impl ImpitBuilder { self } + /// Whether requests go through a proxy: the one set with [`with_proxy`](Self::with_proxy), or one + /// that `reqwest` reads from the environment for HTTPS URLs. + fn uses_proxy(&self) -> bool { + !self.proxy_url.is_empty() + || ["HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"] + .iter() + .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty())) + } + /// Builds the [`Impit`] instance. pub fn build(self) -> Result, ImpitError> { Impit::new(self) @@ -316,7 +328,12 @@ impl Impit { } /// Creates a new [`Impit`] instance based on the options stored in the [`ImpitBuilder`] instance. - fn new(config: ImpitBuilder) -> Result { + fn new(mut config: ImpitBuilder) -> Result { + if config.max_http_version == Version::HTTP_3 && config.uses_proxy() { + warn!("HTTP/3 is disabled because a proxy is set: QUIC requests would skip the proxy"); + config.max_http_version = Version::HTTP_2; + } + let mut h3_client: Option = None; let mut base_client = Self::new_reqwest_client(&config)?; @@ -700,3 +717,30 @@ impl Impit { } } } + +#[cfg(test)] +mod tests { + use reqwest::cookie::Jar; + + use super::Impit; + use crate::{errors::ImpitError, request::RequestOptions}; + + #[tokio::test] + async fn http3_is_disabled_with_a_proxy() { + let impit = Impit::::builder() + .with_http3() + .with_proxy("http://127.0.0.1:9".to_string()) + .build() + .unwrap(); + assert!(impit.h3_client.is_none()); + + let options = RequestOptions { + http3_prior_knowledge: true, + ..Default::default() + }; + let response = impit + .get("https://example.com".to_string(), None, Some(options)) + .await; + assert!(matches!(response, Err(ImpitError::Http3Disabled))); + } +}