Skip to main content

domain_check_lib/
error.rs

1//! Error handling for domain checking operations.
2//!
3//! This module defines a comprehensive error type that covers all the different
4//! ways domain checking can fail, from network issues to invalid input.
5
6use std::fmt;
7
8/// Main error type for domain checking operations.
9///
10/// This enum covers all possible failure modes in the domain checking process,
11/// providing detailed context for debugging and user-friendly error messages.
12#[derive(Debug, Clone)]
13pub enum DomainCheckError {
14    /// Invalid domain name format
15    InvalidDomain { domain: String, reason: String },
16
17    /// Network-related errors (connection, timeout, etc.)
18    NetworkError {
19        message: String,
20        source: Option<String>,
21    },
22
23    /// RDAP protocol specific errors
24    RdapError {
25        domain: String,
26        message: String,
27        status_code: Option<u16>,
28    },
29
30    /// WHOIS protocol specific errors
31    WhoisError { domain: String, message: String },
32
33    /// Bootstrap registry lookup failures
34    BootstrapError { tld: String, message: String },
35
36    /// JSON parsing errors for RDAP responses
37    ParseError {
38        message: String,
39        content: Option<String>,
40    },
41
42    /// Configuration errors (invalid settings, etc.)
43    ConfigError { message: String },
44
45    /// File I/O errors when reading domain lists
46    FileError { path: String, message: String },
47
48    /// Timeout errors when operations take too long
49    Timeout {
50        operation: String,
51        duration: std::time::Duration,
52    },
53
54    /// Rate limiting errors when servers reject requests
55    RateLimited {
56        service: String,
57        message: String,
58        retry_after: Option<std::time::Duration>,
59    },
60
61    /// Invalid pattern syntax in domain generation
62    InvalidPattern { pattern: String, reason: String },
63
64    /// Generic internal errors that don't fit other categories
65    Internal { message: String },
66}
67
68impl DomainCheckError {
69    /// Create a new invalid domain error.
70    pub fn invalid_domain<D: Into<String>, R: Into<String>>(domain: D, reason: R) -> Self {
71        Self::InvalidDomain {
72            domain: domain.into(),
73            reason: reason.into(),
74        }
75    }
76
77    /// Create a new network error.
78    pub fn network<M: Into<String>>(message: M) -> Self {
79        Self::NetworkError {
80            message: message.into(),
81            source: None,
82        }
83    }
84
85    /// Create a new network error with source information.
86    pub fn network_with_source<M: Into<String>, S: Into<String>>(message: M, source: S) -> Self {
87        Self::NetworkError {
88            message: message.into(),
89            source: Some(source.into()),
90        }
91    }
92
93    /// Create a new RDAP error.
94    pub fn rdap<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
95        Self::RdapError {
96            domain: domain.into(),
97            message: message.into(),
98            status_code: None,
99        }
100    }
101
102    /// Create a new RDAP error with HTTP status code.
103    pub fn rdap_with_status<D: Into<String>, M: Into<String>>(
104        domain: D,
105        message: M,
106        status_code: u16,
107    ) -> Self {
108        Self::RdapError {
109            domain: domain.into(),
110            message: message.into(),
111            status_code: Some(status_code),
112        }
113    }
114
115    /// Create a new WHOIS error.
116    pub fn whois<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
117        Self::WhoisError {
118            domain: domain.into(),
119            message: message.into(),
120        }
121    }
122
123    /// Create a new bootstrap error.
124    pub fn bootstrap<T: Into<String>, M: Into<String>>(tld: T, message: M) -> Self {
125        Self::BootstrapError {
126            tld: tld.into(),
127            message: message.into(),
128        }
129    }
130
131    /// Create a new timeout error.
132    pub fn timeout<O: Into<String>>(operation: O, duration: std::time::Duration) -> Self {
133        Self::Timeout {
134            operation: operation.into(),
135            duration,
136        }
137    }
138
139    /// Create a new invalid pattern error.
140    pub fn invalid_pattern<P: Into<String>, R: Into<String>>(pattern: P, reason: R) -> Self {
141        Self::InvalidPattern {
142            pattern: pattern.into(),
143            reason: reason.into(),
144        }
145    }
146
147    /// Create a new internal error.
148    pub fn internal<M: Into<String>>(message: M) -> Self {
149        Self::Internal {
150            message: message.into(),
151        }
152    }
153
154    /// Create a new file error.
155    pub fn file_error<P: Into<String>, M: Into<String>>(path: P, message: M) -> Self {
156        Self::FileError {
157            path: path.into(),
158            message: message.into(),
159        }
160    }
161
162    /// Check if this error indicates the domain is definitely available.
163    ///
164    /// Some error conditions (like NXDOMAIN) actually indicate availability.
165    pub fn indicates_available(&self) -> bool {
166        match self {
167            Self::RdapError {
168                status_code: Some(404),
169                ..
170            } => true,
171            Self::WhoisError { message, .. } => {
172                let msg = message.to_lowercase();
173                msg.contains("not found")
174                    || msg.contains("no match")
175                    || msg.contains("no data found")
176                    || msg.contains("domain available")
177            }
178            _ => false,
179        }
180    }
181
182    /// Check if this error suggests the operation should be retried.
183    pub fn is_retryable(&self) -> bool {
184        matches!(
185            self,
186            Self::NetworkError { .. }
187                | Self::Timeout { .. }
188                | Self::RateLimited { .. }
189                | Self::RdapError {
190                    status_code: Some(500..=599),
191                    ..
192                }
193        )
194        // InvalidPattern is not retryable — it's a user input error
195    }
196}
197
198impl fmt::Display for DomainCheckError {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            Self::InvalidDomain { domain, reason } => {
202                write!(f, "❌ '{}' is not a valid domain name: {}\n   💡 Try something like 'example.com' or use a different domain", domain, reason)
203            }
204            Self::NetworkError { message, source } => {
205                if message.to_lowercase().contains("connection") || message.to_lowercase().contains("connect") {
206                    write!(f, "🌐 Cannot connect to the internet\n   💡 Please check your network connection and try again")
207                } else if message.to_lowercase().contains("timeout") {
208                    write!(f, "⏱️ Request timed out\n   💡 Your internet connection may be slow. Try again or check fewer domains at once")
209                } else {
210                    match source {
211                        Some(_) => write!(f, "🌐 Network error: {}\n   💡 Please check your internet connection", message),
212                        None => write!(f, "🌐 Network error: {}\n   💡 Please check your internet connection", message),
213                    }
214                }
215            }
216            Self::RdapError { domain, message, status_code } => {
217                match status_code {
218                    Some(404) => write!(f, "✅ {}: Domain appears to be available", domain),
219                    Some(429) => write!(f, "⏳ {}: Registry is rate limiting requests\n   💡 Please wait a moment and try again", domain),
220                    Some(500..=599) => write!(f, "⚠️ {}: Registry server is temporarily unavailable\n   💡 Trying backup method...", domain),
221                    Some(code) => write!(f, "⚠️ {}: Registry returned error (HTTP {})\n   💡 This domain registry may be temporarily unavailable", domain, code),
222                    None => write!(f, "⚠️ {}: {}\n   💡 Trying alternative checking method...", domain, message),
223                }
224            }
225            Self::WhoisError { domain, message } => {
226                if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no match") {
227                    write!(f, "✅ {}: Domain appears to be available", domain)
228                } else if message.to_lowercase().contains("rate limit") || message.to_lowercase().contains("too many") {
229                    write!(f, "⏳ {}: WHOIS server is rate limiting requests\n   💡 Please wait a moment and try again", domain)
230                } else if message.to_lowercase().contains("whois") && message.to_lowercase().contains("not found") {
231                    write!(f, "⚠️ {}: WHOIS command not found on this system\n   💡 Please install whois or use online domain checkers", domain)
232                } else {
233                    write!(f, "⚠️ {}: WHOIS lookup failed\n   💡 This may indicate the domain is available or the server is busy", domain)
234                }
235            }
236            Self::BootstrapError { tld, message: _ } => {
237                write!(f, "❓ Unknown domain extension '.{}'\n   💡 This TLD may not support automated checking. Try manually checking with a registrar", tld)
238            }
239            Self::ParseError { message: _, content: _ } => {
240                write!(f, "⚠️ Unable to understand server response\n   💡 The domain registry may be experiencing issues. Please try again later")
241            }
242            Self::ConfigError { message } => {
243                write!(f, "⚙️ Configuration error: {}\n   💡 Please check your command line arguments or configuration file values", message)
244            }
245            Self::FileError { path, message } => {
246                if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no such file") {
247                    write!(f, "📁 File not found: {}\n   💡 Please check the file path and make sure the file exists", path)
248                } else if message.to_lowercase().contains("permission") {
249                    write!(f, "🔒 Permission denied: {}\n   💡 Please check file permissions or try running with appropriate access", path)
250                } else if message.to_lowercase().contains("no valid domains") {
251                    write!(f, "📄 No valid domains found in: {}\n   💡 Make sure the file contains domain names (one per line) and check the format", path)
252                } else {
253                    write!(f, "📁 File error ({}): {}\n   💡 Please check the file and try again", path, message)
254                }
255            }
256            Self::Timeout { operation, duration } => {
257                write!(f, "⏱️ Operation timed out after {:?}: {}\n   💡 Try reducing the number of domains or check your internet connection", duration, operation)
258            }
259            Self::RateLimited { service, message, retry_after } => {
260                match retry_after {
261                    Some(retry) => write!(f, "⏳ Rate limited by {}: {}\n   💡 Please wait {:?} and try again", service, message, retry),
262                    None => write!(f, "⏳ Rate limited by {}: {}\n   💡 Please wait a moment and try again", service, message),
263                }
264            }
265            Self::InvalidPattern { pattern, reason } => {
266                write!(f, "⚙️ Invalid pattern '{}': {}\n   💡 Supported: \\w (letters+hyphen), \\d (digits), ? (alphanumeric), literal characters", pattern, reason)
267            }
268            Self::Internal { message } => {
269                write!(f, "🔧 Internal error: {}\n   💡 This is unexpected. Please try again or report this issue", message)
270            }
271        }
272    }
273}
274
275impl std::error::Error for DomainCheckError {}
276
277// Implement From conversions for common error types
278impl From<reqwest::Error> for DomainCheckError {
279    fn from(err: reqwest::Error) -> Self {
280        if err.is_timeout() {
281            Self::timeout("HTTP request", std::time::Duration::from_secs(30))
282        } else if err.is_connect() {
283            Self::network_with_source("Connection failed", err.to_string())
284        } else {
285            Self::network_with_source("HTTP request failed", err.to_string())
286        }
287    }
288}
289
290impl From<serde_json::Error> for DomainCheckError {
291    fn from(err: serde_json::Error) -> Self {
292        Self::ParseError {
293            message: format!("JSON parsing failed: {}", err),
294            content: None,
295        }
296    }
297}
298
299impl From<std::io::Error> for DomainCheckError {
300    fn from(err: std::io::Error) -> Self {
301        Self::Internal {
302            message: format!("I/O error: {}", err),
303        }
304    }
305}
306
307impl From<regex::Error> for DomainCheckError {
308    fn from(err: regex::Error) -> Self {
309        Self::Internal {
310            message: format!("Regex error: {}", err),
311        }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_invalid_pattern_display() {
321        let err = DomainCheckError::invalid_pattern("test\\x", "unknown escape sequence '\\x'");
322        let msg = format!("{}", err);
323        assert!(msg.contains("test\\x"));
324        assert!(msg.contains("unknown escape sequence"));
325        assert!(msg.contains("\\w")); // hint
326        assert!(msg.contains("\\d")); // hint
327    }
328
329    #[test]
330    fn test_invalid_pattern_not_retryable() {
331        let err = DomainCheckError::invalid_pattern("bad", "reason");
332        assert!(!err.is_retryable());
333    }
334
335    #[test]
336    fn test_invalid_pattern_not_available() {
337        let err = DomainCheckError::invalid_pattern("bad", "reason");
338        assert!(!err.indicates_available());
339    }
340
341    #[test]
342    fn test_rdap_404_indicates_available() {
343        let err = DomainCheckError::rdap_with_status("test.com", "not found", 404);
344        assert!(err.indicates_available());
345    }
346
347    #[test]
348    fn test_network_error_is_retryable() {
349        let err = DomainCheckError::network("connection refused");
350        assert!(err.is_retryable());
351    }
352
353    #[test]
354    fn test_timeout_is_retryable() {
355        let err = DomainCheckError::timeout("test", std::time::Duration::from_secs(5));
356        assert!(err.is_retryable());
357    }
358
359    #[test]
360    fn test_config_error_not_retryable() {
361        let err = DomainCheckError::ConfigError {
362            message: "bad config".to_string(),
363        };
364        assert!(!err.is_retryable());
365    }
366
367    #[test]
368    fn test_rdap_500_is_retryable() {
369        let err = DomainCheckError::rdap_with_status("test.com", "server error", 500);
370        assert!(err.is_retryable());
371    }
372
373    #[test]
374    fn test_rdap_403_not_retryable() {
375        let err = DomainCheckError::rdap_with_status("test.com", "forbidden", 403);
376        assert!(!err.is_retryable());
377    }
378}