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(detail) => format!("WHOIS lookup failed: {}", detail),
105 SeerError::WhoisServerNotFound(detail) => {
106 format!("WHOIS server not found for this TLD: {}", detail)
107 }
108 SeerError::WhoisConnectionFailed(detail) => {
109 format!("WHOIS connection failed: {}", detail)
110 }
111 SeerError::RdapError(detail) => format!("RDAP lookup failed: {}", detail),
112 SeerError::RdapBootstrapError(detail) => {
113 format!("RDAP service unavailable for this resource: {}", detail)
114 }
115 SeerError::DnsError(detail) => format!("DNS resolution failed: {}", detail),
116 SeerError::DnsResolverError(detail) => format!("DNS resolution failed: {}", detail),
117 SeerError::InvalidDomain(domain) => format!("Invalid domain name: {}", domain),
118 SeerError::DomainNotAllowed { tld, .. } => {
119 format!("Domain not allowed: TLD '{}' is not in the allowlist", tld)
120 }
121 SeerError::InvalidIpAddress(ip) => format!("Invalid IP address: {}", ip),
122 SeerError::InvalidRecordType(rt) => format!("Invalid record type: {}", rt),
123 SeerError::HttpError(detail) => format!("HTTP request failed: {}", detail),
124 SeerError::ReqwestError(detail) => format!("HTTP request failed: {}", detail),
125 SeerError::JsonError(detail) => format!("Response parsing failed: {}", detail),
126 SeerError::Timeout(detail) => format!("Operation timed out: {}", detail),
127 SeerError::RateLimited(detail) => {
128 format!("Rate limited - please try again later: {}", detail)
129 }
130 SeerError::CertificateError(detail) => {
131 format!("Certificate validation failed: {}", detail)
132 }
133 SeerError::SslError(detail) => format!("SSL inspection failed: {}", detail),
134 SeerError::BulkOperationError { context, .. } => {
135 format!("Bulk operation partially failed: {}", context)
136 }
137 SeerError::LookupFailed {
138 domain, details, ..
139 } => {
140 format!("Lookup failed for {}: {}", domain, details)
141 }
142 SeerError::ConfigError(msg) => format!("Configuration error: {}", msg),
143 SeerError::InvalidInput(msg) => format!("Invalid input: {}", msg),
144 SeerError::Other(detail) => format!("Operation failed: {}", detail),
145 SeerError::RetryExhausted {
146 attempts,
147 last_error,
148 } => {
149 format!(
150 "Operation failed after {} attempts: {}",
151 attempts,
152 last_error.sanitized_message()
153 )
154 }
155 }
156 }
157}
158
159pub type Result<T> = std::result::Result<T, SeerError>;
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn http_error_sanitized_includes_detail() {
167 let err = SeerError::HttpError("CT log query failed: 502 Bad Gateway".into());
172 let msg = err.sanitized_message();
173 assert!(
174 msg.contains("HTTP request failed"),
175 "expected category prefix; got: {msg}"
176 );
177 assert!(
178 msg.contains("502 Bad Gateway"),
179 "expected detail preserved; got: {msg}"
180 );
181 }
182
183 #[test]
184 fn timeout_sanitized_includes_detail() {
185 let err = SeerError::Timeout("connection to whois.example.com timed out".into());
190 let msg = err.sanitized_message();
191 assert!(msg.contains("Operation timed out"), "got: {msg}");
192 assert!(msg.contains("whois.example.com"), "got: {msg}");
193 }
194
195 #[test]
196 fn dns_error_sanitized_includes_detail() {
197 let err = SeerError::DnsError("invalid nameserver IP: foo.example".into());
204 let msg = err.sanitized_message();
205 assert!(
206 msg.contains("DNS resolution failed"),
207 "expected category prefix; got: {msg}"
208 );
209 assert!(
210 msg.contains("invalid nameserver IP"),
211 "expected detail to be preserved; got: {msg}"
212 );
213 }
214
215 #[test]
216 fn ssl_error_sanitized_includes_detail() {
217 let err = SeerError::SslError(
223 "could not resolve example.com for SSL inspection: DNS resolution failed".into(),
224 );
225 let msg = err.sanitized_message();
226 assert!(
227 msg.contains("SSL inspection failed"),
228 "expected category prefix; got: {msg}"
229 );
230 assert!(
231 msg.contains("DNS resolution failed"),
232 "expected detail to be preserved; got: {msg}"
233 );
234 }
235}