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(detail) => {
95 format!("WHOIS server not found for this TLD: {}", detail)
96 }
97 SeerError::WhoisConnectionFailed(_) => "WHOIS connection failed".to_string(),
98 SeerError::RdapError(_) => "RDAP lookup failed".to_string(),
99 SeerError::RdapBootstrapError(_) => {
100 "RDAP service unavailable for this resource".to_string()
101 }
102 SeerError::DnsError(detail) => format!("DNS resolution failed: {}", detail),
103 SeerError::DnsResolverError(detail) => format!("DNS resolution failed: {}", detail),
104 SeerError::InvalidDomain(domain) => format!("Invalid domain name: {}", domain),
105 SeerError::DomainNotAllowed { tld, .. } => {
106 format!("Domain not allowed: TLD '{}' is not in the allowlist", tld)
107 }
108 SeerError::InvalidIpAddress(ip) => format!("Invalid IP address: {}", ip),
109 SeerError::InvalidRecordType(rt) => format!("Invalid record type: {}", rt),
110 SeerError::HttpError(_) => "HTTP request failed".to_string(),
111 SeerError::ReqwestError(_) => "HTTP request failed".to_string(),
112 SeerError::JsonError(_) => "Response parsing failed".to_string(),
113 SeerError::Timeout(_) => "Operation timed out".to_string(),
114 SeerError::RateLimited(_) => "Rate limited - please try again later".to_string(),
115 SeerError::CertificateError(_) => "Certificate validation failed".to_string(),
116 SeerError::SslError(detail) => format!("SSL inspection failed: {}", detail),
117 SeerError::BulkOperationError { .. } => "Bulk operation partially failed".to_string(),
118 SeerError::LookupFailed { domain, .. } => format!("Lookup failed for {}", domain),
119 SeerError::ConfigError(msg) => format!("Configuration error: {}", msg),
120 SeerError::InvalidInput(msg) => format!("Invalid input: {}", msg),
121 SeerError::Other(_) => "Operation failed".to_string(),
122 SeerError::RetryExhausted {
123 attempts,
124 last_error,
125 } => {
126 format!(
127 "Operation failed after {} attempts: {}",
128 attempts,
129 last_error.sanitized_message()
130 )
131 }
132 }
133 }
134}
135
136pub type Result<T> = std::result::Result<T, SeerError>;
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn dns_error_sanitized_includes_detail() {
144 let err = SeerError::DnsError("invalid nameserver IP: foo.example".into());
151 let msg = err.sanitized_message();
152 assert!(
153 msg.contains("DNS resolution failed"),
154 "expected category prefix; got: {msg}"
155 );
156 assert!(
157 msg.contains("invalid nameserver IP"),
158 "expected detail to be preserved; got: {msg}"
159 );
160 }
161
162 #[test]
163 fn ssl_error_sanitized_includes_detail() {
164 let err = SeerError::SslError(
170 "could not resolve example.com for SSL inspection: DNS resolution failed".into(),
171 );
172 let msg = err.sanitized_message();
173 assert!(
174 msg.contains("SSL inspection failed"),
175 "expected category prefix; got: {msg}"
176 );
177 assert!(
178 msg.contains("DNS resolution failed"),
179 "expected detail to be preserved; got: {msg}"
180 );
181 }
182}