How does hyper::client::HttpConnector handle DNS resolution compared to custom connector implementations?

hyper::client::HttpConnector performs synchronous DNS resolution by default using the system's resolver, while custom connectors can implement asynchronous DNS resolution, use alternative DNS servers, provide custom caching strategies, or integrate with service discovery systems. The key difference is that HttpConnector uses blocking DNS lookups in a thread pool, whereas custom connectors have full control over the resolution process and can optimize for specific use cases.

The HttpConnector Role

use hyper::client::HttpConnector;
use hyper::Client;
 
// HttpConnector is the default connector for hyper clients
// It handles:
// 1. DNS resolution (hostname to IP address)
// 2. TCP connection establishment
// 3. TLS handshake (with optional TLS configuration)
 
fn basic_http_connector() {
    // Create a client with the default HttpConnector
    let client = Client::new();
    
    // This uses HttpConnector with:
    // - System DNS resolver (blocking, in thread pool)
    // - Default TCP configuration
    // - No TLS (HTTP only)
    
    // For HTTPS, use HttpsConnector which wraps HttpConnector
}

HttpConnector is the default connector that handles DNS resolution and TCP connection establishment.

Default DNS Resolution Behavior

use hyper::client::HttpConnector;
use hyper::Client;
 
fn default_dns_behavior() {
    // HttpConnector uses the system's DNS resolver
    // - getaddrinfo on Unix systems
    // - Windows DNS API on Windows
    
    let connector = HttpConnector::new();
    
    // DNS resolution is synchronous/blocking
    // hyper runs it in a thread pool to avoid blocking the async runtime
    
    // The default behavior:
    // 1. Parse the URL to extract hostname and port
    // 2. Call system resolver to get IP addresses
    // 3. Try connecting to resolved addresses
    // 4. Return first successful connection
}

The default connector uses system DNS with blocking resolution in a thread pool.

Custom Connector Trait

use hyper::service::Service;
use hyper::Uri;
use tower::Service as TowerService;
use std::task::{Context, Poll};
use std::future::Future;
 
// A custom connector implements Service<Uri>
// It returns a connected stream (impl AsyncRead + AsyncWrite)
 
pub trait Connector: Service<Uri> {
    // The connector trait is essentially Service<Uri>
    // with Output = impl AsyncRead + AsyncWrite + Send + 'static
}
 
// Custom connector structure
pub struct CustomConnector {
    // Custom state: DNS cache, resolver config, etc.
    dns_cache: DnsCache,
}
 
impl Service<Uri> for CustomConnector {
    type Response = impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Send;
    type Error = std::io::Error;
    type Future = impl Future<Output = Result<Self::Response, Self::Error>>;
    
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // Check if connector is ready to handle new requests
        Poll::Ready(Ok(()))
    }
    
    fn call(&mut self, uri: Uri) -> Self::Future {
        // Custom DNS resolution logic
        async move {
            // 1. Extract hostname from URI
            // 2. Resolve using custom logic
            // 3. Establish connection
            todo!()
        }
    }
}

Custom connectors implement Service<Uri> and have full control over DNS resolution.

DNS Resolution Differences

use hyper::client::HttpConnector;
use std::net::IpAddr;
 
fn dns_resolution_comparison() {
    // ┌─────────────────────────────────────────────────────────────────────────┐
    // │ Aspect               │ HttpConnector              │ Custom Connector   │
    // ├─────────────────────────────────────────────────────────────────────────┤
    // │ Resolution method    │ System resolver (blocking) │ Any method         │
    // │ Async support        │ Thread pool emulation      │ True async         │
    // │ Caching              │ None (OS-level only)       │ Custom caching     │
    // │ DNS server           │ System configured          │ Any server         │
    // │ Timeout control      │ Limited                    │ Full control       │
    // │ Service discovery    │ None                       │ Fully custom       │
    // │ DNS-over-HTTPS       │ No                         │ Implementable      │
    // │ Load balancing       │ Client-side (first IP)     │ Custom strategies  │
    // └─────────────────────────────────────────────────────────────────────────┘
}

Custom connectors enable capabilities that HttpConnector doesn't support natively.

HttpConnector Configuration

use hyper::client::HttpConnector;
use std::time::Duration;
 
fn http_connector_config() {
    // HttpConnector has limited configuration options
    
    let mut connector = HttpConnector::new();
    
    // Set connection timeout
    connector.set_connect_timeout(Some(Duration::from_secs(30)));
    
    // Enable connection reuse (keep-alive)
    connector.set_keepalive(true);
    
    // Set keep-alive timeout
    connector.set_keepalive_timeout(Some(Duration::from_secs(60)));
    
    // Configure nodelay (disable Nagle's algorithm)
    connector.set_nodelay(true);
    
    // Set local address for binding
    // connector.set_local_address(Some(local_addr));
    
    // But you CANNOT:
    // - Change DNS resolution method
    // - Add DNS caching
    // - Use custom DNS servers
    // - Implement service discovery
    // - Add DNS-over-HTTPS/TLS
}

HttpConnector allows connection-level configuration but not DNS customization.

Custom Connector with Async DNS

use hyper::Uri;
use hyper::client::connect::dns::GaiResolver;
use tower::Service;
use std::task::{Context, Poll};
use std::net::SocketAddr;
use std::future::Future;
 
// A custom connector with async DNS resolution
 
pub struct AsyncDnsConnector<R = GaiResolver> {
    // The DNS resolver
    resolver: R,
    // Connection timeout
    connect_timeout: Option<std::time::Duration>,
}
 
impl<R> Service<Uri> for AsyncDnsConnector<R>
where
    R: DnsResolver + Clone + Send + Sync + 'static,
{
    type Response = tokio::net::TcpStream;
    type Error = std::io::Error;
    type Future = impl Future<Output = Result<Self::Response, Self::Error>>;
    
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }
    
    fn call(&mut self, uri: Uri) -> Self::Future {
        let resolver = self.resolver.clone();
        let timeout = self.connect_timeout;
        
        async move {
            // Extract host and port
            let host = uri.host().ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing host")
            })?;
            let port = uri.port_u16().unwrap_or(80);
            
            // Resolve DNS asynchronously
            let addrs = resolver.resolve(host).await?;
            
            // Try each address until connection succeeds
            for addr in addrs {
                let socket_addr = SocketAddr::new(addr, port);
                
                match connect_with_timeout(socket_addr, timeout).await {
                    Ok(stream) => return Ok(stream),
                    Err(_) => continue,
                }
            }
            
            Err(std::io::Error::new(
                std::io::ErrorKind::AddrNotAvailable,
                "all addresses failed"
            ))
        }
    }
}
 
async fn connect_with_timeout(
    addr: SocketAddr,
    timeout: Option<std::time::Duration>,
) -> Result<tokio::net::TcpStream, std::io::Error> {
    match timeout {
        Some(d) => tokio::time::timeout(d, tokio::net::TcpStream::connect(&addr))
            .await
            .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "connection timeout"))?,
        None => tokio::net::TcpStream::connect(&addr).await,
    }
}
 
pub trait DnsResolver {
    fn resolve(&self, host: &str) -> impl Future<Output = Result<Vec<IpAddr>, std::io::Error>> + Send;
}

Custom connectors can implement true async DNS resolution without blocking.

DNS Caching in Custom Connectors

use std::collections::HashMap;
use std::net::IpAddr;
use std::time::Instant;
use std::sync::Arc;
use tokio::sync::RwLock;
 
// DNS cache entry
struct CachedDns {
    addrs: Vec<IpAddr>,
    expires_at: Instant,
}
 
// Cached DNS resolver
pub struct CachedDnsResolver {
    cache: Arc<RwLock<HashMap<String, CachedDns>>>,
    // Underlying resolver (e.g., system resolver or DNS-over-HTTPS)
    underlying: GaiResolver,
    ttl: std::time::Duration,
}
 
impl CachedDnsResolver {
    pub fn new(ttl: std::time::Duration) -> Self {
        Self {
            cache: Arc::new(RwLock::new(HashMap::new())),
            underlying: GaiResolver::new(),
            ttl,
        }
    }
}
 
impl DnsResolver for CachedDnsResolver {
    async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, std::io::Error> {
        // Check cache first
        {
            let cache = self.cache.read().await;
            if let Some(cached) = cache.get(host) {
                if Instant::now() < cached.expires_at {
                    return Ok(cached.addrs.clone());
                }
            }
        }
        
        // Resolve using underlying resolver
        let addrs = self.underlying.resolve(host).await?;
        
        // Cache the result
        {
            let mut cache = self.cache.write().await;
            cache.insert(
                host.to_string(),
                CachedDns {
                    addrs: addrs.clone(),
                    expires_at: Instant::now() + self.ttl,
                },
            );
        }
        
        Ok(addrs)
    }
}

DNS caching reduces resolution latency and DNS server load.

Custom DNS Server Configuration

use std::net::SocketAddr;
use trust_dns_resolver::TokioAsyncResolver;
use trust_dns_resolver::config::*;
 
// Custom connector using specific DNS servers
 
pub struct CustomDnsConnector {
    resolver: TokioAsyncResolver,
}
 
impl CustomDnsConnector {
    pub fn new(dns_servers: Vec<SocketAddr>) -> Self {
        let mut resolver_config = ResolverConfig::new();
        
        for server in dns_servers {
            resolver_config.add_name_server(NameServerConfig {
                socket_addr: server,
                protocol: Protocol::Udp,
                tls_config: None,
            });
        }
        
        let resolver_opts = ResolverOpts::default();
        
        let resolver = TokioAsyncResolver::tokio(resolver_config, resolver_opts);
        
        Self { resolver }
    }
    
    pub async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, std::io::Error> {
        self.resolver
            .lookup_ip(host)
            .await
            .map(|lookup| lookup.iter().collect())
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
    }
}
 
// Usage: Use Google DNS servers
fn use_custom_dns_servers() {
    let connector = CustomDnsConnector::new(vec![
        "8.8.8.8:53".parse().unwrap(),
        "8.8.4.4:53".parse().unwrap(),
    ]);
}

Custom connectors can specify DNS servers instead of using system defaults.

DNS-over-HTTPS Implementation

// Custom connector with DNS-over-HTTPS for privacy
 
pub struct DohConnector {
    doh_endpoint: String,
    http_client: reqwest::Client,
}
 
impl DohConnector {
    pub fn new(doh_endpoint: &str) -> Self {
        Self {
            doh_endpoint: doh_endpoint.to_string(),
            http_client: reqwest::Client::new(),
        }
    }
    
    pub async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, std::io::Error> {
        // Use DNS-over-HTTPS (RFC 8484)
        let url = format!("{}?name={}&type=A", self.doh_endpoint, host);
        
        let response = self.http_client
            .get(&url)
            .header("Accept", "application/dns-message")
            .send()
            .await
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
        
        // Parse DNS response (simplified)
        // Real implementation would parse DNS wire format
        
        todo!("Parse DNS-over-HTTPS response")
    }
}
 
// Usage: Use Cloudflare DNS-over-HTTPS
fn use_doh() {
    let connector = DohConnector::new("https://cloudflare-dns.com/dns-query");
}

DNS-over-HTTPS provides privacy and bypasses network-level DNS filtering.

Service Discovery Integration

use std::collections::HashMap;
 
// Custom connector with service discovery (e.g., Consul, Kubernetes)
 
pub struct ServiceDiscoveryConnector {
    discovery_client: ServiceDiscoveryClient,
    dns_fallback: GaiResolver,
}
 
pub struct ServiceDiscoveryClient {
    // Client for Consul, Kubernetes API, etc.
    services: HashMap<String, Vec<SocketAddr>>,
}
 
impl ServiceDiscoveryConnector {
    pub async fn resolve(&self, host: &str) -> Result<Vec<SocketAddr>, std::io::Error> {
        // Check service discovery first
        if let Some(addrs) = self.discovery_client.resolve_service(host).await? {
            return Ok(addrs);
        }
        
        // Fall back to DNS for external hosts
        let ip_addrs = self.dns_fallback.resolve(host).await?;
        let port = 80; // Would need to determine from context
        Ok(ip_addrs.into_iter().map(|ip| SocketAddr::new(ip, port)).collect())
    }
}
 
// Usage: Connect to services by name without DNS
fn service_discovery_example() {
    // "my-service.internal" resolves directly from service registry
    // No DNS lookup needed
}

Service discovery connectors bypass DNS entirely for internal services.

Performance Implications

use hyper::client::HttpConnector;
 
fn performance_comparison() {
    // ┌─────────────────────────────────────────────────────────────────────────┐
    // │ Aspect               │ HttpConnector          │ Custom Connector        │
    // ├─────────────────────────────────────────────────────────────────────────┤
    // │ First request       │ DNS lookup + connect   │ Depends on impl         │
    // │ Cached DNS          │ OS cache only          │ Custom cache control    │
    // │ Thread usage        │ Thread pool for DNS    │ Async, no threads       │
    // │ Memory overhead     │ Minimal                │ Depends on cache        │
    // │ Latency             │ Variable (DNS)         │ Can optimize             │
    // │ Throughput          │ Good                   │ Can be better            │
    // └─────────────────────────────────────────────────────────────────────────┘
    
    // HttpConnector thread pool:
    // - Uses blocking getaddrinfo
    // - Runs in separate thread pool
    // - Default pool size varies by system
    // - Each DNS call blocks a thread
    
    // Custom async connector:
    // - Uses async DNS (e.g., trust-dns)
    // - No thread pool needed
    // - Better for high concurrency
    // - Can implement caching
}

Custom connectors can significantly improve performance for high-concurrency scenarios.

Error Handling Differences

use hyper::client::HttpConnector;
use std::io;
 
fn error_handling() {
    // HttpConnector error handling:
    // - Returns io::Error with basic context
    // - Limited visibility into DNS failures
    // - Cannot distinguish DNS timeout vs connection timeout
    
    // Custom connector error handling:
    // - Can return rich error types
    // - Distinguish DNS errors from connection errors
    // - Include resolution details in errors
    
    pub enum ConnectorError {
        DnsResolutionFailed {
            host: String,
            source: Box<dyn std::error::Error + Send + Sync>,
        },
        DnsTimeout {
            host: String,
            duration: std::time::Duration,
        },
        ConnectionFailed {
            addresses: Vec<SocketAddr>,
            errors: Vec<io::Error>,
        },
        ConnectionTimeout {
            address: SocketAddr,
            duration: std::time::Duration,
        },
    }
}

Custom connectors can provide more detailed error information.

Load Balancing Strategies

use std::net::SocketAddr;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
 
// Custom connector with load balancing across resolved addresses
 
pub struct LoadBalancedConnector {
    dns_resolver: CachedDnsResolver,
    // Round-robin counter per host
    counters: HashMap<String, AtomicUsize>,
}
 
impl LoadBalancedConnector {
    async fn resolve_and_balance(&self, host: &str, port: u16) -> Result<tokio::net::TcpStream, io::Error> {
        let addrs = self.dns_resolver.resolve(host).await?;
        
        if addrs.is_empty() {
            return Err(io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses"));
        }
        
        // Round-robin through addresses
        let counter = self.counters.entry(host.to_string()).or_insert(AtomicUsize::new(0));
        let index = counter.fetch_add(1, Ordering::Relaxed) % addrs.len();
        
        let addr = SocketAddr::new(addrs[index], port);
        tokio::net::TcpStream::connect(addr).await
    }
}
 
// HttpConnector connects to first successful address
// LoadBalancedConnector distributes across addresses

Custom connectors can implement client-side load balancing across resolved addresses.

Complete Summary

use hyper::client::HttpConnector;
 
fn complete_summary() {
    // ┌─────────────────────────────────────────────────────────────────────────┐
    // │ Feature                  │ HttpConnector        │ Custom Connector    │
    // ├─────────────────────────────────────────────────────────────────────────┤
    // │ DNS resolution           │ System (blocking)    │ Fully customizable  │
    // │ Async                    │ Thread pool          │ Native async       │
    // │ Caching                  │ OS-level             │ Custom TTL/cache    │
    // │ DNS servers              │ System config        │ Any server          │
    // │ DNS-over-HTTPS/TLS       │ No                   │ Implementable       │
    // │ Service discovery        │ No                   │ Fully supported     │
    // │ Load balancing           │ First address        │ Custom strategies   │
    // │ Error detail             │ Basic                │ Rich/custom         │
    // │ Configuration            │ Limited              │ Full control        │
    // │ Complexity               │ Simple               │ More code           │
    // └─────────────────────────────────────────────────────────────────────────┘
    
    // Use HttpConnector when:
    // - System DNS is acceptable
    // - Simple use case
    // - No custom DNS requirements
    // - Default behavior is fine
    
    // Use custom connector when:
    // - Need async DNS resolution
    // - Require DNS caching
    // - Must use specific DNS servers
    // - Need DNS-over-HTTPS/TLS
    // - Integrating service discovery
    // - Implementing load balancing
    // - Want detailed error information
}
 
// Key insight:
// hyper::client::HttpConnector uses synchronous system DNS resolution
// (getaddrinfo on Unix, Windows DNS API) executed in a thread pool.
// This is simple but limited:
//
// - No DNS caching beyond OS-level
// - No control over DNS servers
// - No DNS-over-HTTPS/TLS support
// - Blocks threads during resolution
// - Limited error information
//
// Custom connectors implement Service<Uri> and can:
//
// 1. Use async DNS resolvers (trust-dns, custom implementations)
//    - No thread pool needed
//    - Better for high concurrency
//
// 2. Cache DNS results
//    - Reduce DNS lookups
//    - Lower latency
//    - Custom TTL per domain
//
// 3. Use specific DNS servers
//    - Google DNS, Cloudflare, internal servers
//    - Avoid ISP DNS issues
//
// 4. Implement DNS-over-HTTPS/TLS
//    - Privacy from network observers
//    - Bypass DNS filtering
//
// 5. Integrate service discovery
//    - Consul, Kubernetes, Zookeeper
//    - Direct service-to-service communication
//
// 6. Provide rich errors
//    - Distinguish DNS vs connection errors
//    - Include resolution details
//    - Better debugging
//
// 7. Load balance across addresses
//    - Round-robin, weighted, health-based
//    - Client-side load distribution
//
// The trade-off is complexity: HttpConnector works for most cases,
// while custom connectors require more code but enable advanced features.

Key insight: HttpConnector uses blocking system DNS resolution in a thread pool, which is simple but limited to OS-level caching and configuration. Custom connectors implementing Service<Uri> can provide async DNS resolution, custom caching with configurable TTL, specific DNS servers (including DNS-over-HTTPS), service discovery integration, client-side load balancing, and rich error handling. Use HttpConnector for simple cases where system DNS is acceptable; implement custom connectors when you need async resolution, caching, privacy features, or service discovery.