use thiserror::Error;
#[derive(Error, Debug)]
pub enum SeerError {
#[error("WHOIS lookup failed: {0}")]
WhoisError(String),
#[error("WHOIS server not found for TLD: {0}")]
WhoisServerNotFound(String),
#[error("WHOIS connection failed: {0}")]
WhoisConnectionFailed(String),
#[error("RDAP lookup failed: {0}")]
RdapError(String),
#[error("RDAP bootstrap failed: {0}")]
RdapBootstrapError(String),
#[error("DNS resolution failed: {0}")]
DnsError(String),
#[error("DNS resolver error: {0}")]
DnsResolverError(#[from] hickory_resolver::net::NetError),
#[error("Invalid domain name: {0}")]
InvalidDomain(String),
#[error("Domain not allowed: TLD '{tld}' is not in the allowlist")]
DomainNotAllowed { domain: String, tld: String },
#[error("Invalid IP address: {0}")]
InvalidIpAddress(String),
#[error("Invalid record type: {0}")]
InvalidRecordType(String),
#[error("HTTP request failed: {0}")]
HttpError(String),
#[error("Reqwest error: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("JSON parsing failed: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Timeout: {0}")]
Timeout(String),
#[error("Rate limited: {0}")]
RateLimited(String),
#[error("Certificate error: {0}")]
CertificateError(String),
#[error("SSL error: {0}")]
SslError(String),
#[error("Bulk operation failed: {context}")]
BulkOperationError {
context: String,
failures: Vec<(String, String)>,
},
#[error("Lookup failed for {domain}: {details}\n\nTip: Try checking the registry directly at: {registry_url}")]
LookupFailed {
domain: String,
details: String,
registry_url: String,
},
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("{0}")]
Other(String),
#[error("Operation failed after {attempts} attempts: {last_error}")]
RetryExhausted {
attempts: usize,
last_error: Box<SeerError>,
},
}
impl SeerError {
pub fn sanitized_message(&self) -> String {
match self {
SeerError::WhoisError(detail) => format!("WHOIS lookup failed: {}", detail),
SeerError::WhoisServerNotFound(detail) => {
format!("WHOIS server not found for this TLD: {}", detail)
}
SeerError::WhoisConnectionFailed(detail) => {
format!("WHOIS connection failed: {}", detail)
}
SeerError::RdapError(detail) => format!("RDAP lookup failed: {}", detail),
SeerError::RdapBootstrapError(detail) => {
format!("RDAP service unavailable for this resource: {}", detail)
}
SeerError::DnsError(detail) => format!("DNS resolution failed: {}", detail),
SeerError::DnsResolverError(detail) => format!("DNS resolution failed: {}", detail),
SeerError::InvalidDomain(domain) => format!("Invalid domain name: {}", domain),
SeerError::DomainNotAllowed { tld, .. } => {
format!("Domain not allowed: TLD '{}' is not in the allowlist", tld)
}
SeerError::InvalidIpAddress(ip) => format!("Invalid IP address: {}", ip),
SeerError::InvalidRecordType(rt) => format!("Invalid record type: {}", rt),
SeerError::HttpError(detail) => format!("HTTP request failed: {}", detail),
SeerError::ReqwestError(detail) => format!("HTTP request failed: {}", detail),
SeerError::JsonError(detail) => format!("Response parsing failed: {}", detail),
SeerError::Timeout(detail) => format!("Operation timed out: {}", detail),
SeerError::RateLimited(detail) => {
format!("Rate limited - please try again later: {}", detail)
}
SeerError::CertificateError(detail) => {
format!("Certificate validation failed: {}", detail)
}
SeerError::SslError(detail) => format!("SSL inspection failed: {}", detail),
SeerError::BulkOperationError { context, .. } => {
format!("Bulk operation partially failed: {}", context)
}
SeerError::LookupFailed {
domain, details, ..
} => {
format!("Lookup failed for {}: {}", domain, details)
}
SeerError::ConfigError(msg) => format!("Configuration error: {}", msg),
SeerError::InvalidInput(msg) => format!("Invalid input: {}", msg),
SeerError::Other(detail) => format!("Operation failed: {}", detail),
SeerError::RetryExhausted {
attempts,
last_error,
} => {
format!(
"Operation failed after {} attempts: {}",
attempts,
last_error.sanitized_message()
)
}
}
}
}
pub type Result<T> = std::result::Result<T, SeerError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_error_sanitized_includes_detail() {
let err = SeerError::HttpError("CT log query failed: 502 Bad Gateway".into());
let msg = err.sanitized_message();
assert!(
msg.contains("HTTP request failed"),
"expected category prefix; got: {msg}"
);
assert!(
msg.contains("502 Bad Gateway"),
"expected detail preserved; got: {msg}"
);
}
#[test]
fn timeout_sanitized_includes_detail() {
let err = SeerError::Timeout("connection to whois.example.com timed out".into());
let msg = err.sanitized_message();
assert!(msg.contains("Operation timed out"), "got: {msg}");
assert!(msg.contains("whois.example.com"), "got: {msg}");
}
#[test]
fn dns_error_sanitized_includes_detail() {
let err = SeerError::DnsError("invalid nameserver IP: foo.example".into());
let msg = err.sanitized_message();
assert!(
msg.contains("DNS resolution failed"),
"expected category prefix; got: {msg}"
);
assert!(
msg.contains("invalid nameserver IP"),
"expected detail to be preserved; got: {msg}"
);
}
#[test]
fn ssl_error_sanitized_includes_detail() {
let err = SeerError::SslError(
"could not resolve example.com for SSL inspection: DNS resolution failed".into(),
);
let msg = err.sanitized_message();
assert!(
msg.contains("SSL inspection failed"),
"expected category prefix; got: {msg}"
);
assert!(
msg.contains("DNS resolution failed"),
"expected detail to be preserved; got: {msg}"
);
}
}