Skip to main content

modo/dns/
error.rs

1//! DNS-specific error variants used to classify query failures.
2
3use std::fmt;
4
5/// Error variants for DNS query failures.
6///
7/// Each variant maps to a stable string code (see [`DnsError::code`]) that is
8/// preserved through the [`crate::Error`] pipeline via
9/// [`crate::Error::with_code`]. Use [`crate::Error::source_as::<DnsError>`]
10/// before the response is sent and [`crate::Error::error_code`] after.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum DnsError {
13    /// The DNS query exceeded the configured timeout.
14    Timeout,
15    /// The DNS server returned a SERVFAIL response code.
16    ServerFailure,
17    /// The DNS server refused the query.
18    Refused,
19    /// The DNS response could not be parsed or had a mismatched query ID.
20    Malformed,
21    /// A network-level error occurred (socket bind, send, or receive failure).
22    NetworkError,
23    /// The domain name or token argument was empty or invalid.
24    InvalidInput,
25}
26
27impl DnsError {
28    /// Returns a stable, namespaced string code for this error.
29    ///
30    /// Codes have the form `"dns:<variant>"`, e.g. `"dns:timeout"`.
31    pub fn code(&self) -> &'static str {
32        match self {
33            Self::Timeout => "dns:timeout",
34            Self::ServerFailure => "dns:server_failure",
35            Self::Refused => "dns:refused",
36            Self::Malformed => "dns:malformed",
37            Self::NetworkError => "dns:network_error",
38            Self::InvalidInput => "dns:invalid_input",
39        }
40    }
41}
42
43impl fmt::Display for DnsError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Timeout => write!(f, "dns query timed out"),
47            Self::ServerFailure => write!(f, "dns server failure"),
48            Self::Refused => write!(f, "dns query refused"),
49            Self::Malformed => write!(f, "dns response malformed"),
50            Self::NetworkError => write!(f, "dns network error"),
51            Self::InvalidInput => write!(f, "invalid dns input"),
52        }
53    }
54}
55
56impl std::error::Error for DnsError {}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::Error;
62
63    #[test]
64    fn all_variants_have_unique_codes() {
65        let variants = [
66            DnsError::Timeout,
67            DnsError::ServerFailure,
68            DnsError::Refused,
69            DnsError::Malformed,
70            DnsError::NetworkError,
71            DnsError::InvalidInput,
72        ];
73        let mut codes: Vec<&str> = variants.iter().map(|v| v.code()).collect();
74        let len_before = codes.len();
75        codes.sort();
76        codes.dedup();
77        assert_eq!(codes.len(), len_before, "duplicate error codes found");
78    }
79
80    #[test]
81    fn all_codes_start_with_dns_prefix() {
82        let variants = [
83            DnsError::Timeout,
84            DnsError::ServerFailure,
85            DnsError::NetworkError,
86        ];
87        for v in &variants {
88            assert!(
89                v.code().starts_with("dns:"),
90                "code {} missing prefix",
91                v.code()
92            );
93        }
94    }
95
96    #[test]
97    fn display_is_human_readable() {
98        assert_eq!(DnsError::Timeout.to_string(), "dns query timed out");
99        assert_eq!(DnsError::ServerFailure.to_string(), "dns server failure");
100        assert_eq!(DnsError::Malformed.to_string(), "dns response malformed");
101    }
102
103    #[test]
104    fn recoverable_via_source_as() {
105        let err = Error::bad_gateway("dns server failure")
106            .chain(DnsError::ServerFailure)
107            .with_code(DnsError::ServerFailure.code());
108        let dns_err = err.source_as::<DnsError>();
109        assert_eq!(dns_err, Some(&DnsError::ServerFailure));
110        assert_eq!(err.error_code(), Some("dns:server_failure"));
111    }
112
113    #[test]
114    fn timeout_maps_to_gateway_timeout() {
115        let err = Error::gateway_timeout("dns query timed out")
116            .chain(DnsError::Timeout)
117            .with_code(DnsError::Timeout.code());
118        assert_eq!(err.status(), http::StatusCode::GATEWAY_TIMEOUT);
119        assert_eq!(err.error_code(), Some("dns:timeout"));
120    }
121}