use std::time::Duration;
#[derive(Clone, Debug)]
pub struct TransportConfig {
pub pool_max_idle_per_host: usize,
pub pool_idle_timeout: Option<Duration>,
pub tcp_keepalive: Option<Duration>,
pub http2_prior_knowledge: bool,
pub dns_cache_ttl: Option<Duration>,
#[cfg(feature = "http3")]
pub enable_http3: bool,
pub disable_proxies: bool,
}
impl Default for TransportConfig {
fn default() -> Self {
Self {
pool_max_idle_per_host: 32,
pool_idle_timeout: Some(Duration::from_secs(90)),
tcp_keepalive: Some(Duration::from_secs(60)),
http2_prior_knowledge: false,
dns_cache_ttl: Some(Duration::from_secs(30)),
#[cfg(feature = "http3")]
enable_http3: false,
disable_proxies: false,
}
}
}
impl TransportConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_pool_max_idle_per_host(mut self, count: usize) -> Self {
self.pool_max_idle_per_host = count;
self
}
pub fn with_pool_idle_timeout(mut self, timeout: Option<Duration>) -> Self {
self.pool_idle_timeout = timeout;
self
}
pub fn with_tcp_keepalive(mut self, interval: Option<Duration>) -> Self {
self.tcp_keepalive = interval;
self
}
pub fn with_http2_prior_knowledge(mut self, enabled: bool) -> Self {
self.http2_prior_knowledge = enabled;
self
}
pub fn with_dns_cache_ttl(mut self, ttl: Option<Duration>) -> Self {
self.dns_cache_ttl = ttl;
self
}
#[cfg(feature = "http3")]
pub fn with_http3(mut self, enabled: bool) -> Self {
self.enable_http3 = enabled;
self
}
pub fn with_proxies_disabled(mut self, disabled: bool) -> Self {
self.disable_proxies = disabled;
self
}
#[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
pub fn apply_to_builder(&self, builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let builder = builder
.pool_max_idle_per_host(self.pool_max_idle_per_host)
.pool_idle_timeout(self.pool_idle_timeout)
.tcp_keepalive(self.tcp_keepalive);
let builder = if self.http2_prior_knowledge {
builder.http2_prior_knowledge()
} else {
builder
};
let builder = if self.disable_proxies {
builder.no_proxy()
} else {
builder
};
#[cfg(feature = "http3")]
let _ = self.enable_http3;
builder
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = TransportConfig::default();
assert_eq!(cfg.pool_max_idle_per_host, 32);
assert_eq!(cfg.pool_idle_timeout, Some(Duration::from_secs(90)));
assert_eq!(cfg.tcp_keepalive, Some(Duration::from_secs(60)));
assert!(!cfg.http2_prior_knowledge);
assert_eq!(cfg.dns_cache_ttl, Some(Duration::from_secs(30)));
#[cfg(feature = "http3")]
assert!(!cfg.enable_http3);
assert!(!cfg.disable_proxies);
}
#[test]
fn test_builder_chain() {
let cfg = TransportConfig::new()
.with_pool_max_idle_per_host(16)
.with_pool_idle_timeout(Some(Duration::from_secs(45)))
.with_tcp_keepalive(Some(Duration::from_secs(120)))
.with_http2_prior_knowledge(true)
.with_dns_cache_ttl(Some(Duration::from_secs(60)));
assert_eq!(cfg.pool_max_idle_per_host, 16);
assert_eq!(cfg.pool_idle_timeout, Some(Duration::from_secs(45)));
assert_eq!(cfg.tcp_keepalive, Some(Duration::from_secs(120)));
assert!(cfg.http2_prior_knowledge);
assert_eq!(cfg.dns_cache_ttl, Some(Duration::from_secs(60)));
}
#[test]
fn test_disable_pooling() {
let cfg = TransportConfig::new().with_pool_max_idle_per_host(0);
assert_eq!(cfg.pool_max_idle_per_host, 0);
}
#[test]
fn test_disable_keepalive() {
let cfg = TransportConfig::new().with_tcp_keepalive(None);
assert_eq!(cfg.tcp_keepalive, None);
}
#[test]
fn test_disable_dns_cache() {
let cfg = TransportConfig::new().with_dns_cache_ttl(None);
assert_eq!(cfg.dns_cache_ttl, None);
}
#[cfg(feature = "http3")]
#[test]
fn test_http3_flag() {
let cfg = TransportConfig::new().with_http3(true);
assert!(cfg.enable_http3);
}
#[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
#[test]
fn test_apply_to_builder_builds_client_with_non_default_config() {
let cfg = TransportConfig::new()
.with_pool_max_idle_per_host(128)
.with_pool_idle_timeout(Some(Duration::from_secs(45)))
.with_tcp_keepalive(Some(Duration::from_secs(30)))
.with_http2_prior_knowledge(false)
.with_dns_cache_ttl(Some(Duration::from_secs(60)));
let builder = reqwest::Client::builder();
let builder = cfg.apply_to_builder(builder);
let client = builder.build();
assert!(
client.is_ok(),
"reqwest::Client::build() failed with non-default TransportConfig"
);
}
#[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
#[test]
fn test_apply_to_builder_with_http2_prior_knowledge() {
let cfg = TransportConfig::new().with_http2_prior_knowledge(true);
let client = cfg.apply_to_builder(reqwest::Client::builder()).build();
assert!(
client.is_ok(),
"reqwest::Client::build() failed with http2_prior_knowledge=true"
);
}
#[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
#[test]
fn test_apply_to_builder_with_pooling_disabled() {
let cfg = TransportConfig::new()
.with_pool_max_idle_per_host(0)
.with_pool_idle_timeout(None)
.with_tcp_keepalive(None)
.with_dns_cache_ttl(None);
let client = cfg.apply_to_builder(reqwest::Client::builder()).build();
assert!(client.is_ok(), "reqwest::Client::build() failed with pooling disabled");
}
#[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
#[tokio::test]
async fn test_disable_proxies_bypasses_configured_proxy() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn serve_once(listener: tokio::net::TcpListener) {
let (mut socket, _) = listener.accept().await.expect("accept");
let mut buf = [0u8; 1024];
let _ = socket.read(&mut buf).await;
let _ = socket
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\norigin")
.await;
}
let origin = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind origin");
let origin_url = format!("http://{}/", origin.local_addr().expect("origin address"));
let proxy = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind proxy");
let proxy_url = format!("http://{}", proxy.local_addr().expect("proxy address"));
let proxy_hit = tokio::spawn(async move { proxy.accept().await.is_ok() });
let origin_task = tokio::spawn(serve_once(origin));
let builder = reqwest::Client::builder().proxy(reqwest::Proxy::all(&proxy_url).expect("proxy URL"));
let client = TransportConfig::new()
.with_proxies_disabled(true)
.apply_to_builder(builder)
.build()
.expect("client builds");
let body = client
.get(&origin_url)
.send()
.await
.expect("request reaches origin")
.text()
.await
.expect("body");
assert_eq!(body, "origin");
origin_task.await.expect("origin served");
assert!(!proxy_hit.is_finished(), "the proxy must not be contacted");
proxy_hit.abort();
}
}