pub mod client;
mod config_groups;
pub mod future;
use std::{
borrow::Cow,
collections::HashMap,
convert::TryInto,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
num::NonZeroU32,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
use futures_util::FutureExt;
use http::header::{HeaderMap, HeaderValue, USER_AGENT};
use tower::{
Layer, Service, ServiceBuilder, ServiceExt,
retry::{Retry, RetryLayer},
util::{BoxCloneSyncService, BoxCloneSyncServiceLayer, Either, MapErr, Oneshot},
};
#[cfg(feature = "cookies")]
use {super::layer::cookie::CookieServiceLayer, crate::cookie};
#[cfg(feature = "boring")]
pub(crate) use self::client::extra::ConnectIdentity;
pub(crate) use self::client::{ConnectRequest, HttpClient, extra::ConnectExtra};
pub use self::config_groups::{
HttpVersionPreference, PoolConfigOptions, ProtocolConfigOptions, ProxyConfigOptions,
TlsConfigOptions, TransportConfigOptions,
};
use self::future::Pending;
#[cfg(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate",
))]
use super::layer::decoder::{AcceptEncoding, DecompressionLayer};
#[cfg(any(feature = "ws-yawc", feature = "ws-fastwebsockets"))]
use super::ws::WebSocketRequestBuilder;
use super::{
Body, EmulationFactory,
conn::{
BoxedConnectorLayer, BoxedConnectorService, Conn, Connector, TcpConnectOptions, Unnameable,
},
core::{
body::Incoming,
rt::{TokioExecutor, TokioTimer},
},
layer::{
config::{ConfigService, ConfigServiceLayer, TransportOptions},
recovery::{Recoveries, ResponseRecovery, ResponseRecoveryLayer},
redirect::{FollowRedirect, FollowRedirectLayer},
retry::RetryPolicy,
timeout::{
ResponseBodyTimeout, ResponseBodyTimeoutLayer, Timeout, TimeoutBody, TimeoutLayer,
TimeoutOptions,
},
},
request::{Request, RequestBuilder},
response::Response,
};
#[cfg(feature = "hickory-dns")]
use crate::dns::hickory::HickoryDnsResolver;
#[cfg(feature = "http1")]
use crate::http1::Http1Options;
#[cfg(feature = "http2")]
use crate::http2::Http2Options;
use crate::{
IntoUri, Method, Proxy,
dns::{DnsResolverWithOverrides, DynResolver, GaiResolver, IntoResolve, Resolve},
error::{self, BoxError, Error},
header::OrigHeaderMap,
proxy::Matcher as ProxyMatcher,
redirect::{self, FollowRedirectPolicy},
retry,
tls::{AlpnProtocol, CertStore, Identity, KeyLog, TlsOptions, TlsVersion},
};
#[cfg(not(feature = "cookies"))]
type CookieService<T> = T;
#[cfg(feature = "cookies")]
type CookieService<T> = super::layer::cookie::CookieService<T>;
#[cfg(not(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate"
)))]
type Decompression<T> = T;
#[cfg(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate"
))]
type Decompression<T> = super::layer::decoder::Decompression<T>;
#[cfg(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate"
))]
pub(crate) type InnerResponseBody =
TimeoutBody<tower_http::decompression::DecompressionBody<Incoming>>;
#[cfg(not(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate"
)))]
pub(crate) type InnerResponseBody = TimeoutBody<Incoming>;
type BaseClientService = ResponseBodyTimeout<
ConfigService<
Decompression<
Retry<
RetryPolicy,
FollowRedirect<
CookieService<
MapErr<HttpClient<Connector, Body>, fn(client::error::Error) -> BoxError>,
>,
FollowRedirectPolicy,
>,
>,
>,
>,
>;
pub type ClientService = Timeout<ResponseRecovery<BaseClientService>>;
type HookedClientService =
Timeout<super::layer::hooks::HooksService<ResponseRecovery<BaseClientService>>>;
pub type BoxedClientService =
BoxCloneSyncService<http::Request<Body>, http::Response<super::ClientResponseBody>, BoxError>;
type BoxedClientLayer = BoxCloneSyncServiceLayer<
BoxedClientService,
http::Request<Body>,
http::Response<super::ClientResponseBody>,
BoxError,
>;
pub type ClientRef = Either<ClientService, Either<HookedClientService, BoxedClientService>>;
#[derive(Clone)]
pub struct Client {
inner: Arc<ClientRef>,
}
#[must_use]
pub struct ClientBuilder {
config: CoreConfig,
}
#[repr(u8)]
#[derive(Clone, Debug)]
enum HttpVersionPref {
Http1,
Http2,
All,
}
#[derive(Clone)]
struct TransportConfig {
connect_timeout: Option<Duration>,
connection_verbose: bool,
transport_options: TransportOptions,
tcp_nodelay: bool,
tcp_reuse_address: bool,
tcp_keepalive: Option<Duration>,
tcp_keepalive_interval: Option<Duration>,
tcp_keepalive_retries: Option<u32>,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
tcp_user_timeout: Option<Duration>,
tcp_send_buffer_size: Option<usize>,
tcp_recv_buffer_size: Option<usize>,
tcp_happy_eyeballs_timeout: Option<Duration>,
tcp_connect_options: TcpConnectOptions,
}
#[derive(Clone)]
struct PoolConfig {
idle_timeout: Option<Duration>,
max_idle_per_host: usize,
max_size: Option<NonZeroU32>,
}
#[derive(Clone)]
struct TlsConfig {
keylog: Option<KeyLog>,
tls_info: bool,
tls_sni: bool,
verify_hostname: bool,
identity: Option<Identity>,
cert_store: CertStore,
cert_verification: bool,
min_version: Option<TlsVersion>,
max_version: Option<TlsVersion>,
}
#[derive(Clone)]
struct ProtocolConfig {
http_version_pref: HttpVersionPref,
https_only: bool,
retry_policy: retry::Policy,
redirect_policy: redirect::Policy,
referer: bool,
timeout_options: TimeoutOptions,
recoveries: Recoveries,
}
#[derive(Clone)]
struct ProxyConfig {
proxies: Vec<ProxyMatcher>,
auto_sys_proxy: bool,
}
#[derive(Clone)]
struct DnsConfig {
#[cfg(feature = "hickory-dns")]
hickory_dns: bool,
dns_overrides: HashMap<Cow<'static, str>, Vec<SocketAddr>>,
dns_resolver: Option<Arc<dyn Resolve>>,
}
#[derive(Clone)]
struct MiddlewareConfig {
#[cfg(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate",
))]
accept_encoding: AcceptEncoding,
#[cfg(feature = "cookies")]
cookie_store: Option<Arc<dyn cookie::CookieStore>>,
layers: Vec<BoxedClientLayer>,
connector_layers: Vec<BoxedConnectorLayer>,
hooks: Option<super::layer::hooks::Hooks>,
}
struct CoreConfig {
error: Option<Error>,
headers: HeaderMap,
orig_headers: OrigHeaderMap,
transport: TransportConfig,
pool: PoolConfig,
tls: TlsConfig,
protocol: ProtocolConfig,
proxy: ProxyConfig,
dns: DnsConfig,
middleware: MiddlewareConfig,
}
impl CoreConfig {
fn sync_connect_timeout(&mut self) {
self.protocol
.timeout_options
.timeout_connect(self.transport.connect_timeout);
}
}
impl From<HttpVersionPreference> for HttpVersionPref {
fn from(value: HttpVersionPreference) -> Self {
match value {
HttpVersionPreference::Http1 => Self::Http1,
HttpVersionPreference::Http2 => Self::Http2,
HttpVersionPreference::All => Self::All,
}
}
}
impl TransportConfig {
fn with_transport_options(mut self, transport_options: TransportOptions) -> Self {
self.transport_options = transport_options;
self
}
}
impl From<TransportConfigOptions> for TransportConfig {
fn from(value: TransportConfigOptions) -> Self {
Self {
connect_timeout: value.connect_timeout,
connection_verbose: value.connection_verbose,
transport_options: TransportOptions::default(),
tcp_nodelay: value.tcp_nodelay,
tcp_reuse_address: value.tcp_reuse_address,
tcp_keepalive: value.tcp_keepalive,
tcp_keepalive_interval: value.tcp_keepalive_interval,
tcp_keepalive_retries: value.tcp_keepalive_retries,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
tcp_user_timeout: value.tcp_user_timeout,
tcp_send_buffer_size: value.tcp_send_buffer_size,
tcp_recv_buffer_size: value.tcp_recv_buffer_size,
tcp_happy_eyeballs_timeout: value.tcp_happy_eyeballs_timeout,
tcp_connect_options: value.tcp_connect_options,
}
}
}
impl From<PoolConfigOptions> for PoolConfig {
fn from(value: PoolConfigOptions) -> Self {
Self {
idle_timeout: value.idle_timeout,
max_idle_per_host: value.max_idle_per_host,
max_size: value.max_size,
}
}
}
impl From<TlsConfigOptions> for TlsConfig {
fn from(value: TlsConfigOptions) -> Self {
Self {
keylog: value.keylog,
tls_info: value.tls_info,
tls_sni: value.tls_sni,
verify_hostname: value.verify_hostname,
identity: value.identity,
cert_store: value.cert_store,
cert_verification: value.cert_verification,
min_version: value.min_version,
max_version: value.max_version,
}
}
}
impl From<ProtocolConfigOptions> for ProtocolConfig {
fn from(value: ProtocolConfigOptions) -> Self {
Self {
http_version_pref: value.http_version_preference.into(),
https_only: value.https_only,
retry_policy: value.retry_policy,
redirect_policy: value.redirect_policy,
referer: value.referer,
timeout_options: value.timeout_options,
recoveries: value.recoveries,
}
}
}
impl From<ProxyConfigOptions> for ProxyConfig {
fn from(value: ProxyConfigOptions) -> Self {
Self {
proxies: value.proxies.into_iter().map(Proxy::into_matcher).collect(),
auto_sys_proxy: value.auto_system_proxy,
}
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
#[inline]
pub fn new() -> Client {
Client::builder().build().expect(
"Client::new() failed to build — use Client::builder().build() for error handling",
)
}
pub fn builder() -> ClientBuilder {
ClientBuilder {
config: CoreConfig {
error: None,
headers: HeaderMap::new(),
orig_headers: OrigHeaderMap::new(),
transport: TransportConfig {
connect_timeout: None,
connection_verbose: false,
transport_options: TransportOptions::default(),
tcp_nodelay: true,
tcp_reuse_address: false,
tcp_keepalive: Some(Duration::from_secs(15)),
tcp_keepalive_interval: Some(Duration::from_secs(15)),
tcp_keepalive_retries: Some(3),
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
tcp_user_timeout: Some(Duration::from_secs(30)),
tcp_connect_options: TcpConnectOptions::default(),
tcp_send_buffer_size: None,
tcp_recv_buffer_size: None,
tcp_happy_eyeballs_timeout: Some(Duration::from_millis(300)),
},
pool: PoolConfig {
idle_timeout: Some(Duration::from_secs(90)),
max_idle_per_host: usize::MAX,
max_size: None,
},
tls: TlsConfig {
keylog: None,
tls_info: false,
tls_sni: true,
verify_hostname: true,
identity: None,
cert_store: CertStore::default(),
cert_verification: true,
min_version: None,
max_version: None,
},
protocol: ProtocolConfig {
http_version_pref: HttpVersionPref::All,
https_only: false,
retry_policy: retry::Policy::default(),
redirect_policy: redirect::Policy::none(),
referer: true,
timeout_options: TimeoutOptions::default(),
recoveries: Recoveries::new(),
},
proxy: ProxyConfig {
proxies: Vec::new(),
auto_sys_proxy: true,
},
dns: DnsConfig {
#[cfg(feature = "hickory-dns")]
hickory_dns: cfg!(feature = "hickory-dns"),
dns_overrides: HashMap::new(),
dns_resolver: None,
},
middleware: MiddlewareConfig {
#[cfg(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate",
))]
accept_encoding: AcceptEncoding::default(),
#[cfg(feature = "cookies")]
cookie_store: None,
layers: Vec::new(),
connector_layers: Vec::new(),
hooks: None,
},
},
}
}
#[inline]
pub fn get<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::GET, uri)
}
#[inline]
pub fn post<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::POST, uri)
}
#[inline]
pub fn put<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::PUT, uri)
}
#[inline]
pub fn patch<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::PATCH, uri)
}
#[inline]
pub fn delete<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::DELETE, uri)
}
#[inline]
pub fn head<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::HEAD, uri)
}
#[inline]
pub fn options<U: IntoUri>(&self, uri: U) -> RequestBuilder {
self.request(Method::OPTIONS, uri)
}
pub fn request<U: IntoUri>(&self, method: Method, uri: U) -> RequestBuilder {
let req = uri.into_uri().map(move |uri| Request::new(method, uri));
RequestBuilder::new(self.clone(), req)
}
#[inline]
#[cfg(any(feature = "ws-yawc", feature = "ws-fastwebsockets"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "ws-yawc", feature = "ws-fastwebsockets")))
)]
pub fn websocket<U: IntoUri>(&self, uri: U) -> WebSocketRequestBuilder {
WebSocketRequestBuilder::new(self.request(Method::GET, uri))
}
pub fn execute(&self, request: Request) -> Pending {
let req = http::Request::<Body>::from(request);
let uri = req.uri().clone();
let fut = Oneshot::new(self.inner.as_ref().clone(), req);
Pending::request(uri, fut)
}
pub(crate) fn into_inner(self) -> ClientRef {
Arc::unwrap_or_clone(self.inner)
}
pub(crate) fn clone_inner(&self) -> ClientRef {
self.inner.as_ref().clone()
}
}
impl tower::Service<Request> for Client {
type Response = Response;
type Error = Error;
type Future = Pending;
#[inline(always)]
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
#[inline(always)]
fn call(&mut self, req: Request) -> Self::Future {
self.execute(req)
}
}
impl tower::Service<Request> for &'_ Client {
type Response = Response;
type Error = Error;
type Future = Pending;
#[inline(always)]
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
#[inline(always)]
fn call(&mut self, req: Request) -> Self::Future {
self.execute(req)
}
}
impl ClientBuilder {
#[inline]
pub fn transport_config(mut self, config: TransportConfigOptions) -> ClientBuilder {
let transport_options = self.config.transport.transport_options.clone();
self.config.transport =
TransportConfig::from(config).with_transport_options(transport_options);
self.config.sync_connect_timeout();
self
}
#[inline]
pub fn pool_config(mut self, config: PoolConfigOptions) -> ClientBuilder {
self.config.pool = config.into();
self
}
#[inline]
pub fn tls_config(mut self, config: TlsConfigOptions) -> ClientBuilder {
self.config.tls = config.into();
self
}
#[inline]
pub fn protocol_config(mut self, config: ProtocolConfigOptions) -> ClientBuilder {
self.config.protocol = config.into();
self.config.sync_connect_timeout();
self
}
#[inline]
pub fn proxy_config(mut self, config: ProxyConfigOptions) -> ClientBuilder {
self.config.proxy = config.into();
self
}
pub fn build(self) -> crate::Result<Client> {
let mut config = self.config;
if let Some(err) = config.error {
return Err(err);
}
if config.proxy.auto_sys_proxy {
config.proxy.proxies.push(ProxyMatcher::system());
}
let service = {
let tls_options = config.transport.transport_options.tls_options.take();
#[cfg(feature = "http1")]
let http1_options = config.transport.transport_options.http1_options.take();
#[cfg(feature = "http2")]
let http2_options = config.transport.transport_options.http2_options.take();
let resolver = {
let mut resolver: Arc<dyn Resolve> = match config.dns.dns_resolver {
Some(dns_resolver) => dns_resolver,
#[cfg(feature = "hickory-dns")]
None if config.dns.hickory_dns => Arc::new(HickoryDnsResolver::new()?),
None => Arc::new(GaiResolver::new()),
};
if !config.dns.dns_overrides.is_empty() {
resolver = Arc::new(DnsResolverWithOverrides::new(
resolver,
config.dns.dns_overrides,
));
}
DynResolver::new(resolver)
};
let connector = Connector::builder(config.proxy.proxies, resolver)
.timeout(config.transport.connect_timeout)
.tls_info(config.tls.tls_info)
.tls_options(tls_options)
.verbose(config.transport.connection_verbose)
.with_tls(|tls| {
let alpn_protocol = match config.protocol.http_version_pref {
HttpVersionPref::Http1 => Some(AlpnProtocol::HTTP1),
HttpVersionPref::Http2 => Some(AlpnProtocol::HTTP2),
_ => None,
};
tls.alpn_protocol(alpn_protocol)
.max_version(config.tls.max_version)
.min_version(config.tls.min_version)
.tls_sni(config.tls.tls_sni)
.verify_hostname(config.tls.verify_hostname)
.cert_verification(config.tls.cert_verification)
.cert_store(config.tls.cert_store)
.identity(config.tls.identity)
.keylog(config.tls.keylog)
})
.with_http(|http| {
http.enforce_http(false);
http.set_keepalive(config.transport.tcp_keepalive);
http.set_keepalive_interval(config.transport.tcp_keepalive_interval);
http.set_keepalive_retries(config.transport.tcp_keepalive_retries);
http.set_reuse_address(config.transport.tcp_reuse_address);
http.set_connect_options(config.transport.tcp_connect_options);
http.set_connect_timeout(config.transport.connect_timeout);
http.set_nodelay(config.transport.tcp_nodelay);
http.set_send_buffer_size(config.transport.tcp_send_buffer_size);
http.set_recv_buffer_size(config.transport.tcp_recv_buffer_size);
http.set_happy_eyeballs_timeout(config.transport.tcp_happy_eyeballs_timeout);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
http.set_tcp_user_timeout(config.transport.tcp_user_timeout);
})
.build(config.middleware.connector_layers)?;
#[allow(unused_mut)]
let mut builder = HttpClient::builder(TokioExecutor::new());
#[cfg(feature = "http1")]
{
builder = builder.http1_options(http1_options);
}
#[cfg(feature = "http2")]
{
builder = builder
.http2_options(http2_options)
.http2_only(matches!(
config.protocol.http_version_pref,
HttpVersionPref::Http2
))
.http2_timer(TokioTimer::new());
}
builder
.pool_timer(TokioTimer::new())
.pool_idle_timeout(config.pool.idle_timeout)
.pool_max_idle_per_host(config.pool.max_idle_per_host)
.pool_max_size(config.pool.max_size)
.build(connector)
.map_err(Into::into as _)
};
let client = {
#[cfg(feature = "cookies")]
let service = ServiceBuilder::new()
.layer(CookieServiceLayer::new(config.middleware.cookie_store))
.service(service);
let service = ServiceBuilder::new()
.layer(RetryLayer::new(RetryPolicy::new(
config.protocol.retry_policy,
)))
.layer({
let policy = FollowRedirectPolicy::new(config.protocol.redirect_policy)
.with_referer(config.protocol.referer)
.with_https_only(config.protocol.https_only);
FollowRedirectLayer::with_policy(policy)
})
.service(service);
#[cfg(any(
feature = "gzip",
feature = "zstd",
feature = "brotli",
feature = "deflate",
))]
let service = ServiceBuilder::new()
.layer(DecompressionLayer::new(config.middleware.accept_encoding))
.service(service);
let service = ServiceBuilder::new()
.layer(ResponseRecoveryLayer::new(config.protocol.recoveries))
.layer(ResponseBodyTimeoutLayer::new(
config.protocol.timeout_options,
))
.layer(ConfigServiceLayer::new(
config.protocol.https_only,
config.headers,
config.orig_headers,
))
.service(service);
if config.middleware.layers.is_empty() {
if let Some(hooks) = config.middleware.hooks
&& !hooks.is_empty()
{
let service = ServiceBuilder::new()
.layer(TimeoutLayer::new(config.protocol.timeout_options))
.layer(super::layer::hooks::HooksLayer::new(hooks))
.service(service);
ClientRef::Right(Either::Left(service))
} else {
let service = ServiceBuilder::new()
.layer(TimeoutLayer::new(config.protocol.timeout_options))
.service(service);
ClientRef::Left(service)
}
} else {
let mut service = BoxCloneSyncService::new(service);
if let Some(hooks) = config.middleware.hooks
&& !hooks.is_empty()
{
let hooks_layer = super::layer::hooks::HooksLayer::new(hooks);
service = ServiceBuilder::new()
.layer(BoxCloneSyncServiceLayer::new(hooks_layer))
.service(service);
}
let service = config
.middleware
.layers
.into_iter()
.fold(service, |service, layer| {
ServiceBuilder::new().layer(layer).service(service)
});
let service = ServiceBuilder::new()
.layer(TimeoutLayer::new(config.protocol.timeout_options))
.service(service)
.map_err(error::map_timeout_to_request_error);
ClientRef::Right(Either::Right(BoxCloneSyncService::new(service)))
}
};
Ok(Client {
inner: Arc::new(client),
})
}
pub fn user_agent<V>(mut self, value: V) -> ClientBuilder
where
V: TryInto<HeaderValue>,
V::Error: Into<http::Error>,
{
match value.try_into() {
Ok(value) => {
self.config.headers.insert(USER_AGENT, value);
}
Err(err) => {
self.config.error = Some(Error::builder(err.into()));
}
};
self
}
#[inline]
pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
crate::util::replace_headers(&mut self.config.headers, headers);
self
}
#[inline]
pub fn orig_headers(mut self, orig_headers: OrigHeaderMap) -> ClientBuilder {
self.config.orig_headers.extend(orig_headers);
self
}
#[inline]
#[cfg(feature = "cookies")]
#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))]
pub fn cookie_store(mut self, enable: bool) -> ClientBuilder {
if enable {
self.cookie_provider(Arc::new(cookie::Jar::default()))
} else {
self.config.middleware.cookie_store = None;
self
}
}
#[inline]
#[cfg(feature = "cookies")]
#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))]
pub fn cookie_provider<C>(mut self, cookie_store: C) -> ClientBuilder
where
C: cookie::IntoCookieStore,
{
self.config.middleware.cookie_store = Some(cookie_store.into_cookie_store());
self
}
#[inline]
#[cfg(feature = "gzip")]
#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
pub fn gzip(mut self, enable: bool) -> ClientBuilder {
self.config.middleware.accept_encoding.gzip = enable;
self
}
#[inline]
#[cfg(feature = "brotli")]
#[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
pub fn brotli(mut self, enable: bool) -> ClientBuilder {
self.config.middleware.accept_encoding.brotli = enable;
self
}
#[inline]
#[cfg(feature = "zstd")]
#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
pub fn zstd(mut self, enable: bool) -> ClientBuilder {
self.config.middleware.accept_encoding.zstd = enable;
self
}
#[inline]
#[cfg(feature = "deflate")]
#[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
pub fn deflate(mut self, enable: bool) -> ClientBuilder {
self.config.middleware.accept_encoding.deflate = enable;
self
}
#[inline]
pub fn no_zstd(self) -> ClientBuilder {
#[cfg(feature = "zstd")]
{
self.zstd(false)
}
#[cfg(not(feature = "zstd"))]
{
self
}
}
#[inline]
pub fn no_gzip(self) -> ClientBuilder {
#[cfg(feature = "gzip")]
{
self.gzip(false)
}
#[cfg(not(feature = "gzip"))]
{
self
}
}
#[inline]
pub fn no_brotli(self) -> ClientBuilder {
#[cfg(feature = "brotli")]
{
self.brotli(false)
}
#[cfg(not(feature = "brotli"))]
{
self
}
}
#[inline]
pub fn no_deflate(self) -> ClientBuilder {
#[cfg(feature = "deflate")]
{
self.deflate(false)
}
#[cfg(not(feature = "deflate"))]
{
self
}
}
#[inline]
pub fn redirect(mut self, policy: redirect::Policy) -> ClientBuilder {
self.config.protocol.redirect_policy = policy;
self
}
#[inline]
pub fn referer(mut self, enable: bool) -> ClientBuilder {
self.config.protocol.referer = enable;
self
}
pub fn retry(mut self, policy: retry::Policy) -> ClientBuilder {
self.config.protocol.retry_policy = policy;
self
}
#[inline]
pub fn system_proxy(mut self) -> ClientBuilder {
self.config.proxy.auto_sys_proxy = true;
self
}
#[inline]
pub fn proxy(mut self, proxy: Proxy) -> ClientBuilder {
self.config.proxy.proxies.push(proxy.into_matcher());
self.config.proxy.auto_sys_proxy = false;
self
}
#[inline]
pub fn proxy_pool(mut self, pool: crate::proxy_pool::ProxyPool) -> ClientBuilder {
self.config
.middleware
.layers
.push(BoxCloneSyncServiceLayer::new(pool.layer()));
self.config.proxy.auto_sys_proxy = false;
self
}
#[inline]
pub fn no_proxy(mut self) -> ClientBuilder {
self.config.proxy.proxies.clear();
self.config.proxy.auto_sys_proxy = false;
self
}
#[inline]
pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
self.config.protocol.timeout_options.total_timeout(timeout);
self
}
#[inline]
pub fn read_timeout(mut self, timeout: Duration) -> ClientBuilder {
self.config.protocol.timeout_options.read_timeout(timeout);
self
}
#[inline]
pub fn connect_timeout(mut self, timeout: Duration) -> ClientBuilder {
self.config.transport.connect_timeout = Some(timeout);
self.config.sync_connect_timeout();
self
}
#[inline]
pub fn timeout_global(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config.protocol.timeout_options.timeout_global(timeout);
self
}
#[inline]
pub fn timeout_per_call(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_per_call(timeout);
self
}
#[inline]
pub fn timeout_resolve(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_resolve(timeout);
self
}
#[inline]
pub fn timeout_send_request(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_send_request(timeout);
self
}
#[inline]
pub fn timeout_await_100(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_await_100(timeout);
self
}
#[inline]
pub fn timeout_send_body(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_send_body(timeout);
self
}
#[inline]
pub fn timeout_recv_response(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_recv_response(timeout);
self
}
#[inline]
pub fn timeout_recv_body(mut self, timeout: Option<Duration>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.timeout_recv_body(timeout);
self
}
#[inline]
pub fn max_response_header_size(mut self, size: Option<usize>) -> ClientBuilder {
self.config
.protocol
.timeout_options
.set_max_response_header_size(size);
self
}
#[inline]
pub fn connection_verbose(mut self, verbose: bool) -> ClientBuilder {
self.config.transport.connection_verbose = verbose;
self
}
#[inline]
pub fn pool_idle_timeout<D>(mut self, val: D) -> ClientBuilder
where
D: Into<Option<Duration>>,
{
self.config.pool.idle_timeout = val.into();
self
}
#[inline]
pub fn pool_max_idle_per_host(mut self, max: usize) -> ClientBuilder {
self.config.pool.max_idle_per_host = max;
self
}
#[inline]
pub fn pool_max_size(mut self, max: u32) -> ClientBuilder {
self.config.pool.max_size = NonZeroU32::new(max);
self
}
#[inline]
pub fn https_only(mut self, enabled: bool) -> ClientBuilder {
self.config.protocol.https_only = enabled;
self
}
#[inline]
pub fn http1_only(mut self) -> ClientBuilder {
self.config.protocol.http_version_pref = HttpVersionPref::Http1;
self
}
#[inline]
pub fn http2_only(mut self) -> ClientBuilder {
self.config.protocol.http_version_pref = HttpVersionPref::Http2;
self
}
#[cfg(feature = "http1")]
#[inline]
pub fn http1_options(mut self, options: Http1Options) -> ClientBuilder {
*self.config.transport.transport_options.http1_options_mut() = Some(options);
self
}
#[cfg(feature = "http1")]
#[inline]
pub fn max_poll_iterations(mut self, max_iterations: usize) -> ClientBuilder {
assert!(
max_iterations > 0,
"max_poll_iterations must be greater than zero"
);
let mut options = self
.config
.transport
.transport_options
.http1_options
.take()
.unwrap_or_default();
options.h1_max_poll_iterations = Some(max_iterations);
*self.config.transport.transport_options.http1_options_mut() = Some(options);
self
}
#[cfg(feature = "http2")]
#[inline]
pub fn http2_options(mut self, options: Http2Options) -> ClientBuilder {
*self.config.transport.transport_options.http2_options_mut() = Some(options);
self
}
#[inline]
pub fn tcp_nodelay(mut self, enabled: bool) -> ClientBuilder {
self.config.transport.tcp_nodelay = enabled;
self
}
#[inline]
pub fn tcp_keepalive<D>(mut self, val: D) -> ClientBuilder
where
D: Into<Option<Duration>>,
{
self.config.transport.tcp_keepalive = val.into();
self
}
#[inline]
pub fn tcp_keepalive_interval<D>(mut self, val: D) -> ClientBuilder
where
D: Into<Option<Duration>>,
{
self.config.transport.tcp_keepalive_interval = val.into();
self
}
#[inline]
pub fn tcp_keepalive_retries<C>(mut self, retries: C) -> ClientBuilder
where
C: Into<Option<u32>>,
{
self.config.transport.tcp_keepalive_retries = retries.into();
self
}
#[inline]
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[cfg_attr(
docsrs,
doc(cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))
)]
pub fn tcp_user_timeout<D>(mut self, val: D) -> ClientBuilder
where
D: Into<Option<Duration>>,
{
self.config.transport.tcp_user_timeout = val.into();
self
}
#[inline]
pub fn tcp_reuse_address(mut self, enabled: bool) -> ClientBuilder {
self.config.transport.tcp_reuse_address = enabled;
self
}
#[inline]
pub fn tcp_send_buffer_size<S>(mut self, size: S) -> ClientBuilder
where
S: Into<Option<usize>>,
{
self.config.transport.tcp_send_buffer_size = size.into();
self
}
#[inline]
pub fn tcp_recv_buffer_size<S>(mut self, size: S) -> ClientBuilder
where
S: Into<Option<usize>>,
{
self.config.transport.tcp_recv_buffer_size = size.into();
self
}
#[inline]
pub fn tcp_happy_eyeballs_timeout<D>(mut self, val: D) -> ClientBuilder
where
D: Into<Option<Duration>>,
{
self.config.transport.tcp_happy_eyeballs_timeout = val.into();
self
}
#[inline]
pub fn local_address<T>(mut self, addr: T) -> ClientBuilder
where
T: Into<Option<IpAddr>>,
{
self.config
.transport
.tcp_connect_options
.set_local_address(addr.into());
self
}
#[inline]
pub fn local_addresses<V4, V6>(mut self, ipv4: V4, ipv6: V6) -> ClientBuilder
where
V4: Into<Option<Ipv4Addr>>,
V6: Into<Option<Ipv6Addr>>,
{
self.config
.transport
.tcp_connect_options
.set_local_addresses(ipv4, ipv6);
self
}
#[inline]
#[cfg(any(
target_os = "android",
target_os = "fuchsia",
target_os = "illumos",
target_os = "ios",
target_os = "linux",
target_os = "macos",
target_os = "solaris",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos",
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
target_os = "android",
target_os = "fuchsia",
target_os = "illumos",
target_os = "ios",
target_os = "linux",
target_os = "macos",
target_os = "solaris",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos",
)))
)]
pub fn interface<T>(mut self, interface: T) -> ClientBuilder
where
T: Into<std::borrow::Cow<'static, str>>,
{
self.config
.transport
.tcp_connect_options
.set_interface(interface);
self
}
#[inline]
pub fn identity(mut self, identity: Identity) -> ClientBuilder {
self.config.tls.identity = Some(identity);
self
}
#[inline]
pub fn cert_store(mut self, store: CertStore) -> ClientBuilder {
self.config.tls.cert_store = store;
self
}
#[inline]
pub fn cert_verification(mut self, cert_verification: bool) -> ClientBuilder {
self.config.tls.cert_verification = cert_verification;
self
}
#[inline]
pub fn verify_hostname(mut self, verify_hostname: bool) -> ClientBuilder {
self.config.tls.verify_hostname = verify_hostname;
self
}
#[inline]
pub fn tls_sni(mut self, tls_sni: bool) -> ClientBuilder {
self.config.tls.tls_sni = tls_sni;
self
}
#[inline]
pub fn keylog(mut self, keylog: KeyLog) -> ClientBuilder {
self.config.tls.keylog = Some(keylog);
self
}
#[inline]
pub fn min_tls_version(mut self, version: TlsVersion) -> ClientBuilder {
self.config.tls.min_version = Some(version);
self
}
#[inline]
pub fn max_tls_version(mut self, version: TlsVersion) -> ClientBuilder {
self.config.tls.max_version = Some(version);
self
}
#[inline]
pub fn tls_info(mut self, tls_info: bool) -> ClientBuilder {
self.config.tls.tls_info = tls_info;
self
}
#[inline]
pub fn tls_options(mut self, options: TlsOptions) -> ClientBuilder {
*self.config.transport.transport_options.tls_options_mut() = Some(options);
self
}
#[inline]
#[cfg(feature = "hickory-dns")]
#[cfg_attr(docsrs, doc(cfg(feature = "hickory-dns")))]
pub fn no_hickory_dns(mut self) -> ClientBuilder {
self.config.dns.hickory_dns = false;
self
}
#[inline]
pub fn resolve<D>(self, domain: D, addr: SocketAddr) -> ClientBuilder
where
D: Into<Cow<'static, str>>,
{
self.resolve_to_addrs(domain, std::iter::once(addr))
}
#[inline]
pub fn resolve_to_addrs<D, A>(mut self, domain: D, addrs: A) -> ClientBuilder
where
D: Into<Cow<'static, str>>,
A: IntoIterator<Item = SocketAddr>,
{
self.config
.dns
.dns_overrides
.insert(domain.into(), addrs.into_iter().collect());
self
}
#[inline]
pub fn dns_resolver<R>(mut self, resolver: R) -> ClientBuilder
where
R: IntoResolve,
{
self.config.dns.dns_resolver = Some(resolver.into_resolve());
self
}
#[inline]
pub fn hooks(mut self, hooks: super::layer::hooks::Hooks) -> ClientBuilder {
self.config.middleware.hooks = Some(hooks);
self
}
#[inline]
pub fn recoveries(mut self, recoveries: super::layer::recovery::Recoveries) -> ClientBuilder {
self.config.protocol.recoveries = recoveries;
self
}
#[inline]
pub fn on_request<F>(mut self, hook: F) -> ClientBuilder
where
F: Fn(&mut http::Request<Body>) -> Result<(), Error> + Send + Sync + 'static,
{
let hooks = self
.config
.middleware
.hooks
.get_or_insert_with(super::layer::hooks::Hooks::new);
struct ClosureHook<F>(F);
impl<F> super::layer::hooks::BeforeRequestHook for ClosureHook<F>
where
F: Fn(&mut http::Request<Body>) -> Result<(), Error> + Send + Sync,
{
fn on_request(&self, request: &mut http::Request<Body>) -> Result<(), Error> {
(self.0)(request)
}
}
hooks.before_request.push(Arc::new(ClosureHook(hook)));
self
}
#[inline]
pub fn on_response<F>(mut self, hook: F) -> ClientBuilder
where
F: Fn(http::StatusCode, &http::HeaderMap) -> Result<(), Error> + Send + Sync + 'static,
{
let hooks = self
.config
.middleware
.hooks
.get_or_insert_with(super::layer::hooks::Hooks::new);
struct ClosureHook<F>(F);
impl<F> super::layer::hooks::AfterResponseHook for ClosureHook<F>
where
F: Fn(http::StatusCode, &http::HeaderMap) -> Result<(), Error> + Send + Sync,
{
fn on_response(
&self,
status: http::StatusCode,
headers: &http::HeaderMap,
) -> Result<(), Error> {
(self.0)(status, headers)
}
}
hooks.after_response.push(Arc::new(ClosureHook(hook)));
self
}
#[inline]
pub fn on_status<F, Fut>(mut self, status: http::StatusCode, hook: F) -> ClientBuilder
where
F: Fn(super::layer::recovery::StatusRecoveryContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<http::Request<Body>>, Error>> + Send + 'static,
{
struct ClosureHook<F>(F);
impl<F, Fut> super::layer::recovery::OnStatusHook for ClosureHook<F>
where
F: Fn(super::layer::recovery::StatusRecoveryContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<http::Request<Body>>, Error>> + Send + 'static,
{
fn on_status(
&self,
context: super::layer::recovery::StatusRecoveryContext,
) -> futures_util::future::BoxFuture<'static, Result<Option<http::Request<Body>>, Error>>
{
(self.0)(context).boxed()
}
}
self.config
.protocol
.recoveries
.push_hook(status, Arc::new(ClosureHook(hook)));
self
}
#[inline]
pub fn layer<L>(mut self, layer: L) -> ClientBuilder
where
L: Layer<BoxedClientService> + Clone + Send + Sync + 'static,
L::Service: Service<
http::Request<Body>,
Response = http::Response<super::ClientResponseBody>,
Error = BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<L::Service as Service<http::Request<Body>>>::Future: Send + 'static,
{
let layer = BoxCloneSyncServiceLayer::new(layer);
self.config.middleware.layers.push(layer);
self
}
#[inline]
pub fn connector_layer<L>(mut self, layer: L) -> ClientBuilder
where
L: Layer<BoxedConnectorService> + Clone + Send + Sync + 'static,
L::Service:
Service<Unnameable, Response = Conn, Error = BoxError> + Clone + Send + Sync + 'static,
<L::Service as Service<Unnameable>>::Future: Send + 'static,
{
let layer = BoxCloneSyncServiceLayer::new(layer);
self.config.middleware.connector_layers.push(layer);
self
}
#[inline]
pub fn emulation<P>(mut self, factory: P) -> ClientBuilder
where
P: EmulationFactory,
{
let emulation = factory.emulation();
let (transport_opts, headers, orig_headers) = emulation.into_parts();
self.config
.transport
.transport_options
.apply_transport_options(transport_opts);
self.default_headers(headers).orig_headers(orig_headers)
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, time::Duration};
use tower::util::Either;
use super::*;
struct NoopBeforeRequestHook;
impl super::super::layer::hooks::BeforeRequestHook for NoopBeforeRequestHook {
fn on_request(&self, _request: &mut http::Request<Body>) -> Result<(), Error> {
Ok(())
}
}
#[test]
fn hooks_only_client_keeps_typed_service_path() {
let hooks = super::super::layer::hooks::Hooks::builder()
.before_request(Arc::new(NoopBeforeRequestHook))
.build();
let client = Client::builder().hooks(hooks).build().unwrap();
assert!(matches!(
client.into_inner(),
Either::Right(Either::Left(_))
));
}
#[test]
fn transport_config_options_override_transport_defaults() {
let connect_timeout = Duration::from_secs(3);
let builder = Client::builder().transport_config(
TransportConfigOptions::new()
.connect_timeout(Some(connect_timeout))
.connection_verbose(true)
.tcp_nodelay(false)
.tcp_reuse_address(true),
);
assert_eq!(
builder.config.transport.connect_timeout,
Some(connect_timeout)
);
assert!(builder.config.transport.connection_verbose);
assert!(!builder.config.transport.tcp_nodelay);
assert!(builder.config.transport.tcp_reuse_address);
assert_eq!(
builder.config.protocol.timeout_options.connect_timeout(),
Some(connect_timeout)
);
}
#[test]
fn transport_builder_methods_mutate_nested_transport_group() {
let connect_timeout = Duration::from_secs(7);
let builder = Client::builder()
.connect_timeout(connect_timeout)
.connection_verbose(true);
assert_eq!(
builder.config.transport.connect_timeout,
Some(connect_timeout)
);
assert!(builder.config.transport.connection_verbose);
assert_eq!(
builder.config.protocol.timeout_options.connect_timeout(),
Some(connect_timeout)
);
}
#[test]
fn reusable_protocol_config_can_be_applied_to_multiple_builders() {
let protocol = ProtocolConfigOptions::new().https_only(true).referer(false);
let builder_a = Client::builder().protocol_config(protocol.clone());
let builder_b = Client::builder().protocol_config(protocol);
assert!(builder_a.config.protocol.https_only);
assert!(!builder_a.config.protocol.referer);
assert!(builder_b.config.protocol.https_only);
assert!(!builder_b.config.protocol.referer);
}
#[test]
fn protocol_config_preserves_transport_connect_timeout() {
let connect_timeout = Duration::from_secs(11);
let builder = Client::builder()
.connect_timeout(connect_timeout)
.protocol_config(ProtocolConfigOptions::new().https_only(true));
assert_eq!(
builder.config.transport.connect_timeout,
Some(connect_timeout)
);
assert_eq!(
builder.config.protocol.timeout_options.connect_timeout(),
Some(connect_timeout)
);
}
#[cfg(feature = "http1")]
#[test]
fn transport_config_preserves_existing_http1_transport_options() {
let builder = Client::builder()
.max_poll_iterations(7)
.transport_config(TransportConfigOptions::new().tcp_nodelay(false));
let options = builder
.config
.transport
.transport_options
.http1_options
.unwrap();
assert_eq!(options.h1_max_poll_iterations, Some(7));
assert!(!builder.config.transport.tcp_nodelay);
}
#[cfg(feature = "http1")]
#[test]
fn max_poll_iterations_updates_http1_options() {
let builder = Client::builder().max_poll_iterations(7);
let options = builder
.config
.transport
.transport_options
.http1_options
.unwrap();
assert_eq!(options.h1_max_poll_iterations, Some(7));
}
}