use std::io;
use std::time::Duration;
use crate::domain::Tld;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{input:?} is not a usable domain name: {reason}")]
InvalidDomain {
input: String,
reason: DomainError,
},
#[error("no WHOIS or RDAP service is known for .{tld}")]
UnsupportedTld {
tld: Tld,
},
#[error("no usable endpoint for .{tld}: {detail}")]
NoEndpoint {
tld: Tld,
detail: String,
},
#[error("invalid registry definitions: {0}")]
Definitions(String),
#[error("could not reach {server}: {source}")]
Connect {
server: String,
#[source]
source: io::Error,
},
#[error("transport error talking to {server}: {source}")]
Io {
server: String,
#[source]
source: io::Error,
},
#[error("{server} did not answer within {elapsed:.1?}")]
Timeout {
server: String,
elapsed: Duration,
},
#[error("{server} returned an empty response")]
EmptyResponse {
server: String,
},
#[error("{server} declined the query: {reason}")]
Refused {
server: String,
reason: Refusal,
},
#[error("{url} answered HTTP {status}")]
Http {
url: String,
status: u16,
},
#[error("malformed RDAP response from {url}: {source}")]
Rdap {
url: String,
#[source]
source: serde_json::Error,
},
#[error("no conclusive answer for {domain} (checked {consulted}): {detail}")]
Inconclusive {
domain: String,
consulted: String,
detail: String,
},
#[error("{wanted} requires the `{feature}` cargo feature")]
FeatureDisabled {
feature: &'static str,
wanted: &'static str,
},
}
impl Error {
pub fn is_transient(&self) -> bool {
match self {
Error::Connect { .. } | Error::Io { .. } | Error::Timeout { .. } => true,
Error::EmptyResponse { .. } => true,
Error::Refused { reason, .. } => reason.is_transient(),
Error::Http { status, .. } => *status == 429 || *status >= 500,
_ => false,
}
}
pub fn is_endpoint_failure(&self) -> bool {
matches!(
self,
Error::Connect { .. }
| Error::Io { .. }
| Error::Timeout { .. }
| Error::EmptyResponse { .. }
| Error::Refused { .. }
| Error::Http { .. }
| Error::Rdap { .. }
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DomainError {
#[error("the name is empty")]
Empty,
#[error("the name has no top-level domain")]
NoTld,
#[error("the name is longer than 253 characters")]
TooLong,
#[error("{label:?} is not a valid DNS label")]
InvalidLabel {
label: String,
},
#[error("{label:?} is not a valid internationalised label: {detail}")]
InvalidIdn {
label: String,
detail: String,
},
#[error("the input is an IP address, not a domain name")]
IpAddress,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Refusal {
#[error("query rate limit exceeded")]
RateLimited,
#[error("client blocked by the server")]
Blocked,
#[error("server temporarily unavailable")]
Unavailable,
#[error("port 43 service retired in favour of RDAP")]
PortRetired,
#[error("server does not serve this TLD")]
WrongNamespace,
#[error("server requires interactive or authenticated access")]
AccessRestricted,
}
impl Refusal {
pub fn is_transient(self) -> bool {
matches!(self, Refusal::RateLimited | Refusal::Unavailable)
}
}