1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum SeerError {
5 #[error("WHOIS lookup failed: {0}")]
6 WhoisError(String),
7
8 #[error("WHOIS server not found for TLD: {0}")]
9 WhoisServerNotFound(String),
10
11 #[error("WHOIS connection failed: {0}")]
12 WhoisConnectionFailed(String),
13
14 #[error("RDAP lookup failed: {0}")]
15 RdapError(String),
16
17 #[error("RDAP bootstrap failed: {0}")]
18 RdapBootstrapError(String),
19
20 #[error("DNS resolution failed: {0}")]
21 DnsError(String),
22
23 #[error("DNS resolver error: {0}")]
24 DnsResolverError(#[from] hickory_resolver::net::NetError),
25
26 #[error("Invalid domain name: {0}")]
27 InvalidDomain(String),
28
29 #[error("Domain not allowed: TLD '{tld}' is not in the allowlist")]
30 DomainNotAllowed { domain: String, tld: String },
31
32 #[error("Invalid IP address: {0}")]
33 InvalidIpAddress(String),
34
35 #[error("Invalid record type: {0}")]
36 InvalidRecordType(String),
37
38 #[error("HTTP request failed: {0}")]
39 HttpError(String),
40
41 #[error("Reqwest error: {0}")]
42 ReqwestError(#[from] reqwest::Error),
43
44 #[error("JSON parsing failed: {0}")]
45 JsonError(#[from] serde_json::Error),
46
47 #[error("Timeout: {0}")]
48 Timeout(String),
49
50 #[error("Rate limited: {0}")]
51 RateLimited(String),
52
53 #[error("Certificate error: {0}")]
54 CertificateError(String),
55
56 #[error("SSL error: {0}")]
57 SslError(String),
58
59 #[error("Bulk operation failed: {context}")]
60 BulkOperationError {
61 context: String,
62 failures: Vec<(String, String)>,
63 },
64
65 #[error("Lookup failed for {domain}: {details}\n\nTip: Try checking the registry directly at: {registry_url}")]
66 LookupFailed {
67 domain: String,
68 details: String,
69 registry_url: String,
70 },
71
72 #[error("Configuration error: {0}")]
73 ConfigError(String),
74
75 #[error("Invalid input: {0}")]
76 InvalidInput(String),
77
78 #[error("{0}")]
79 Other(String),
80
81 #[error("Operation failed after {attempts} attempts: {last_error}")]
82 RetryExhausted {
83 attempts: usize,
84 last_error: Box<SeerError>,
85 },
86}
87
88impl SeerError {
89 pub fn sanitized_message(&self) -> String {
92 match self {
93 SeerError::WhoisError(_) => "WHOIS lookup failed".to_string(),
94 SeerError::WhoisServerNotFound(_) => "WHOIS server not found for this TLD".to_string(),
95 SeerError::WhoisConnectionFailed(_) => "WHOIS connection failed".to_string(),
96 SeerError::RdapError(_) => "RDAP lookup failed".to_string(),
97 SeerError::RdapBootstrapError(_) => {
98 "RDAP service unavailable for this resource".to_string()
99 }
100 SeerError::DnsError(_) => "DNS resolution failed".to_string(),
101 SeerError::DnsResolverError(_) => "DNS resolution failed".to_string(),
102 SeerError::InvalidDomain(domain) => format!("Invalid domain name: {}", domain),
103 SeerError::DomainNotAllowed { tld, .. } => {
104 format!("Domain not allowed: TLD '{}' is not in the allowlist", tld)
105 }
106 SeerError::InvalidIpAddress(ip) => format!("Invalid IP address: {}", ip),
107 SeerError::InvalidRecordType(rt) => format!("Invalid record type: {}", rt),
108 SeerError::HttpError(_) => "HTTP request failed".to_string(),
109 SeerError::ReqwestError(_) => "HTTP request failed".to_string(),
110 SeerError::JsonError(_) => "Response parsing failed".to_string(),
111 SeerError::Timeout(_) => "Operation timed out".to_string(),
112 SeerError::RateLimited(_) => "Rate limited - please try again later".to_string(),
113 SeerError::CertificateError(_) => "Certificate validation failed".to_string(),
114 SeerError::SslError(detail) => format!("SSL inspection failed: {}", detail),
115 SeerError::BulkOperationError { .. } => "Bulk operation partially failed".to_string(),
116 SeerError::LookupFailed { domain, .. } => format!("Lookup failed for {}", domain),
117 SeerError::ConfigError(msg) => format!("Configuration error: {}", msg),
118 SeerError::InvalidInput(msg) => format!("Invalid input: {}", msg),
119 SeerError::Other(_) => "Operation failed".to_string(),
120 SeerError::RetryExhausted {
121 attempts,
122 last_error,
123 } => {
124 format!(
125 "Operation failed after {} attempts: {}",
126 attempts,
127 last_error.sanitized_message()
128 )
129 }
130 }
131 }
132}
133
134pub type Result<T> = std::result::Result<T, SeerError>;
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn ssl_error_sanitized_includes_detail() {
142 let err = SeerError::SslError(
148 "could not resolve example.com for SSL inspection: DNS resolution failed".into(),
149 );
150 let msg = err.sanitized_message();
151 assert!(
152 msg.contains("SSL inspection failed"),
153 "expected category prefix; got: {msg}"
154 );
155 assert!(
156 msg.contains("DNS resolution failed"),
157 "expected detail to be preserved; got: {msg}"
158 );
159 }
160}