Skip to main content

oxihttp_client/
resolver.rs

1//! Custom DNS resolver support for oxihttp clients.
2use std::future::Future;
3use std::net::SocketAddr;
4use std::pin::Pin;
5use std::task::{Context, Poll};
6
7use hyper_util::client::legacy::connect::dns::Name;
8use oxihttp_core::OxiHttpError;
9use tower_service::Service;
10
11/// A pluggable async DNS resolver.
12///
13/// Implement this trait to override the default system resolver (getaddrinfo).
14pub trait DnsResolver: Send + Sync + 'static {
15    /// Resolve `name` to a list of socket addresses.
16    fn resolve(
17        &self,
18        name: &str,
19    ) -> Pin<Box<dyn Future<Output = Result<Vec<SocketAddr>, OxiHttpError>> + Send>>;
20}
21
22/// Default DNS resolver using `tokio::net::lookup_host` (wraps getaddrinfo).
23#[derive(Debug, Clone, Default)]
24pub struct GaiDnsResolver;
25
26impl DnsResolver for GaiDnsResolver {
27    fn resolve(
28        &self,
29        name: &str,
30    ) -> Pin<Box<dyn Future<Output = Result<Vec<SocketAddr>, OxiHttpError>> + Send>> {
31        let host = format!("{name}:0");
32        Box::pin(async move {
33            let addrs: Vec<SocketAddr> = tokio::net::lookup_host(host)
34                .await
35                .map_err(OxiHttpError::from)?
36                .collect();
37            Ok(addrs)
38        })
39    }
40}
41
42/// Adapts an `Arc<dyn DnsResolver>` into a `tower_service::Service<Name>` that hyper-util's
43/// `HttpConnector` accepts. This is the bridge between our trait and hyper-util internals.
44///
45/// This is a public type because it appears in the `ResolverClient` and `ResolverHttpsClient`
46/// type aliases, but it is not intended for direct use by library consumers.
47#[derive(Clone)]
48pub struct BoxResolver(pub(crate) std::sync::Arc<dyn DnsResolver>);
49
50/// Iterator adapter for the resolved addresses.
51///
52/// Part of the public interface through `BoxResolver`'s `Service` impl.
53pub struct BoxResolverAddrs(std::vec::IntoIter<SocketAddr>);
54
55impl Iterator for BoxResolverAddrs {
56    type Item = SocketAddr;
57    fn next(&mut self) -> Option<SocketAddr> {
58        self.0.next()
59    }
60}
61
62type BoxError = Box<dyn std::error::Error + Send + Sync>;
63
64impl Service<Name> for BoxResolver {
65    type Response = BoxResolverAddrs;
66    type Error = BoxError;
67    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
68
69    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
70        Poll::Ready(Ok(()))
71    }
72
73    fn call(&mut self, name: Name) -> Self::Future {
74        let resolver = self.0.clone();
75        let host = name.as_str().to_owned();
76        Box::pin(async move {
77            let addrs = resolver
78                .resolve(&host)
79                .await
80                .map_err(|e| Box::new(e) as BoxError)?;
81            Ok(BoxResolverAddrs(addrs.into_iter()))
82        })
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use std::net::SocketAddr;
90
91    struct FixedResolver(Vec<SocketAddr>);
92    impl DnsResolver for FixedResolver {
93        fn resolve(
94            &self,
95            _name: &str,
96        ) -> Pin<Box<dyn Future<Output = Result<Vec<SocketAddr>, OxiHttpError>> + Send>> {
97            let addrs = self.0.clone();
98            Box::pin(async move { Ok(addrs) })
99        }
100    }
101
102    struct EmptyResolver;
103    impl DnsResolver for EmptyResolver {
104        fn resolve(
105            &self,
106            name: &str,
107        ) -> Pin<Box<dyn Future<Output = Result<Vec<SocketAddr>, OxiHttpError>> + Send>> {
108            let name = name.to_owned();
109            Box::pin(async move { Err(OxiHttpError::Dns(format!("no address for {name}"))) })
110        }
111    }
112
113    #[tokio::test]
114    async fn test_gai_resolver() {
115        let r = GaiDnsResolver;
116        let addrs = r.resolve("localhost").await.expect("resolve");
117        assert!(!addrs.is_empty());
118    }
119
120    #[tokio::test]
121    async fn test_fixed_resolver_builds_client() {
122        let resolver = FixedResolver(vec!["127.0.0.1:0".parse().expect("addr")]);
123        let _client = crate::ClientBuilder::new()
124            .with_resolver(resolver)
125            .build_with_resolver()
126            .expect("build");
127    }
128
129    #[tokio::test]
130    async fn test_empty_resolver_error() {
131        let r = EmptyResolver;
132        let result = r.resolve("nonexistent.example").await;
133        assert!(result.is_err());
134        let err = result.expect_err("should be error");
135        assert!(matches!(err, OxiHttpError::Dns(_)));
136    }
137}