use crate::dns::{DnsCache, DnsConfig};
#[cfg(feature = "h2")]
use crate::pool::PoolKey;
#[cfg(feature = "h2")]
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
#[cfg(feature = "h2")]
use std::time::Instant;
use crate::error::{Error, ErrorKind, Result};
use crate::pool::PoolConfig;
#[cfg(feature = "h2")]
use crate::request::ProtocolPolicy;
use crate::request::Request;
use crate::response::Response;
#[cfg(feature = "h2")]
mod connection;
#[cfg(feature = "h2")]
mod frame;
#[cfg(feature = "h2")]
mod transport;
#[derive(Default)]
pub(crate) struct ConnectionPool {
#[cfg(feature = "h2")]
connections: HashMap<PoolKey, Vec<connection::H2ClientConnection>>,
}
impl ConnectionPool {
#[cfg(feature = "h2")]
fn acquire(
&mut self,
key: &PoolKey,
pool_config: PoolConfig,
) -> Option<connection::H2ClientConnection> {
let now = Instant::now();
let connections = self.connections.get_mut(key)?;
connections.retain(|connection| !connection.should_evict(now, pool_config));
let selected = connections
.iter()
.filter(|connection| connection.can_accept_new_stream())
.min_by_key(|connection| connection.load())
.cloned();
if connections.is_empty() {
self.connections.remove(key);
}
selected
}
#[cfg(feature = "h2")]
fn insert(
&mut self,
key: PoolKey,
connection: connection::H2ClientConnection,
pool_config: PoolConfig,
) -> bool {
if pool_config.max_idle_per_host == 0 {
return false;
}
let now = Instant::now();
let connections = self.connections.entry(key).or_default();
connections.retain(|existing| !existing.should_evict(now, pool_config));
if connections
.iter()
.any(|existing| existing.ptr_eq(&connection))
{
return true;
}
if connections.len() >= pool_config.max_idle_per_host {
return false;
}
connections.push(connection);
true
}
pub(crate) fn clear(&mut self) {
#[cfg(feature = "h2")]
{
for connections in self.connections.values() {
for connection in connections {
connection.close();
}
}
self.connections.clear();
}
}
}
#[cfg(feature = "h2")]
pub(crate) async fn execute(
request: Request,
pool: Arc<Mutex<ConnectionPool>>,
dns_cache: Arc<DnsCache>,
dns_config: DnsConfig,
local_addr: Option<SocketAddr>,
pool_config: PoolConfig,
) -> Result<Response> {
connection::execute(
request,
pool,
dns_cache,
dns_config,
local_addr,
pool_config,
)
.await
}
#[cfg(not(feature = "h2"))]
pub(crate) async fn execute(
_request: Request,
_pool: Arc<Mutex<ConnectionPool>>,
_dns_cache: Arc<DnsCache>,
_dns_config: DnsConfig,
_local_addr: Option<SocketAddr>,
_pool_config: PoolConfig,
) -> Result<Response> {
Err(Error::new(
ErrorKind::Transport,
"http2 support requires the h2 feature",
))
}
#[cfg(feature = "h2")]
pub(crate) fn can_attempt_h2(request: &Request) -> bool {
(request.url().scheme() == "https" && request.tls_config().validate_h2_alpn().is_ok())
|| (request.url().scheme() == "http"
&& (request.prior_knowledge_h2c()
|| matches!(
request.protocol_policy(),
ProtocolPolicy::Http2Only | ProtocolPolicy::PreferHttp2
)))
}
#[cfg(not(feature = "h2"))]
pub(crate) fn can_attempt_h2(_request: &Request) -> bool {
false
}
#[cfg(feature = "h2")]
pub(crate) fn clone_request_for_h2(request: &Request) -> Result<Request> {
let body = request.body().try_clone().ok_or_else(|| {
Error::new(
ErrorKind::BodyAlreadyConsumed,
"request body cannot be cloned for http2",
)
})?;
let mut tls_config = request.tls_config().clone();
if let Some(profile) = request.emulation_profile() {
if let Some(fp) = profile.tls_fingerprint() {
if !fp.alpn_protocols.is_empty() {
let protocols = fp
.alpn_protocols
.iter()
.filter(|p| p.as_str() == "h2" || p.as_str() == "http/1.1")
.cloned()
.collect::<Vec<_>>();
if !protocols.is_empty() {
tls_config = tls_config.alpn_protocols(protocols);
}
}
}
}
Ok(Request::new(
request.method(),
request.url().clone(),
request.headers().clone(),
request.cookies().to_vec(),
request.timeout_config(),
request.protocol_policy(),
request.retry_policy(),
request.prior_knowledge_h2c(),
request.progress_callback().cloned(),
request.progress_config(),
request.h2_keepalive_config(),
tls_config,
request.proxy_cloned(),
request.compression_mode(),
body,
request.emulation_profile().cloned(),
))
}
#[cfg(not(feature = "h2"))]
pub(crate) fn clone_request_for_h2(_request: &Request) -> Result<Request> {
Err(Error::new(
ErrorKind::Transport,
"http2 support requires the h2 feature",
))
}
#[cfg(all(test, feature = "h2", feature = "emulation"))]
mod tests {
use super::clone_request_for_h2;
use crate::Emulation;
use crate::client::shared_http1_transport;
use crate::request::{Method, ProtocolPolicy, RequestBuilder};
#[test]
fn clone_request_for_h2_applies_alpn_only_to_clone() {
let original = RequestBuilder::new(shared_http1_transport(), Method::Get, "https://x.test")
.emulation(Emulation::Chrome136)
.build_request()
.unwrap();
assert!(
original
.tls_config()
.validate_http1_alpn(ProtocolPolicy::Auto)
.is_ok()
);
let cloned = clone_request_for_h2(&original).unwrap();
assert_eq!(
cloned.tls_config().validate_h2_alpn().unwrap(),
vec!["h2".to_owned(), "http/1.1".to_owned()]
);
assert!(
cloned
.tls_config()
.validate_http1_alpn(ProtocolPolicy::Auto)
.is_err()
);
}
}