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    /// Generic internal errors that don't fit other categories
62    Internal { message: String },
63}
64
65impl DomainCheckError {
66    /// Create a new invalid domain error.
67    pub fn invalid_domain<D: Into<String>, R: Into<String>>(domain: D, reason: R) -> Self {
68        Self::InvalidDomain {
69            domain: domain.into(),
70            reason: reason.into(),
71        }
72    }
73
74    /// Create a new network error.
75    pub fn network<M: Into<String>>(message: M) -> Self {
76        Self::NetworkError {
77            message: message.into(),
78            source: None,
79        }
80    }
81
82    /// Create a new network error with source information.
83    pub fn network_with_source<M: Into<String>, S: Into<String>>(message: M, source: S) -> Self {
84        Self::NetworkError {
85            message: message.into(),
86            source: Some(source.into()),
87        }
88    }
89
90    /// Create a new RDAP error.
91    pub fn rdap<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
92        Self::RdapError {
93            domain: domain.into(),
94            message: message.into(),
95            status_code: None,
96        }
97    }
98
99    /// Create a new RDAP error with HTTP status code.
100    pub fn rdap_with_status<D: Into<String>, M: Into<String>>(
101        domain: D,
102        message: M,
103        status_code: u16,
104    ) -> Self {
105        Self::RdapError {
106            domain: domain.into(),
107            message: message.into(),
108            status_code: Some(status_code),
109        }
110    }
111
112    /// Create a new WHOIS error.
113    pub fn whois<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
114        Self::WhoisError {
115            domain: domain.into(),
116            message: message.into(),
117        }
118    }
119
120    /// Create a new bootstrap error.
121    pub fn bootstrap<T: Into<String>, M: Into<String>>(tld: T, message: M) -> Self {
122        Self::BootstrapError {
123            tld: tld.into(),
124            message: message.into(),
125        }
126    }
127
128    /// Create a new timeout error.
129    pub fn timeout<O: Into<String>>(operation: O, duration: std::time::Duration) -> Self {
130        Self::Timeout {
131            operation: operation.into(),
132            duration,
133        }
134    }
135
136    /// Create a new internal error.
137    pub fn internal<M: Into<String>>(message: M) -> Self {
138        Self::Internal {
139            message: message.into(),
140        }
141    }
142
143    /// Create a new file error.
144    pub fn file_error<P: Into<String>, M: Into<String>>(path: P, message: M) -> Self {
145        Self::FileError {
146            path: path.into(),
147            message: message.into(),
148        }
149    }
150
151    /// Check if this error indicates the domain is definitely available.
152    ///
153    /// Some error conditions (like NXDOMAIN) actually indicate availability.
154    pub fn indicates_available(&self) -> bool {
155        match self {
156            Self::RdapError {
157                status_code: Some(404),
158                ..
159            } => true,
160            Self::WhoisError { message, .. } => {
161                let msg = message.to_lowercase();
162                msg.contains("not found")
163                    || msg.contains("no match")
164                    || msg.contains("no data found")
165                    || msg.contains("domain available")
166            }
167            _ => false,
168        }
169    }
170
171    /// Check if this error suggests the operation should be retried.
172    pub fn is_retryable(&self) -> bool {
173        matches!(
174            self,
175            Self::NetworkError { .. }
176                | Self::Timeout { .. }
177                | Self::RateLimited { .. }
178                | Self::RdapError {
179                    status_code: Some(500..=599),
180                    ..
181                }
182        )
183    }
184}
185
186impl fmt::Display for DomainCheckError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            Self::InvalidDomain { domain, reason } => {
190                write!(f, "❌ '{}' is not a valid domain name: {}\n   💡 Try something like 'example.com' or use a different domain", domain, reason)
191            }
192            Self::NetworkError { message, source } => {
193                if message.to_lowercase().contains("connection") || message.to_lowercase().contains("connect") {
194                    write!(f, "🌐 Cannot connect to the internet\n   💡 Please check your network connection and try again")
195                } else if message.to_lowercase().contains("timeout") {
196                    write!(f, "⏱️ Request timed out\n   💡 Your internet connection may be slow. Try again or check fewer domains at once")
197                } else {
198                    match source {
199                        Some(_) => write!(f, "🌐 Network error: {}\n   💡 Please check your internet connection", message),
200                        None => write!(f, "🌐 Network error: {}\n   💡 Please check your internet connection", message),
201                    }
202                }
203            }
204            Self::RdapError { domain, message, status_code } => {
205                match status_code {
206                    Some(404) => write!(f, "✅ {}: Domain appears to be available", domain),
207                    Some(429) => write!(f, "⏳ {}: Registry is rate limiting requests\n   💡 Please wait a moment and try again", domain),
208                    Some(500..=599) => write!(f, "⚠️ {}: Registry server is temporarily unavailable\n   💡 Trying backup method...", domain),
209                    Some(code) => write!(f, "⚠️ {}: Registry returned error (HTTP {})\n   💡 This domain registry may be temporarily unavailable", domain, code),
210                    None => write!(f, "⚠️ {}: {}\n   💡 Trying alternative checking method...", domain, message),
211                }
212            }
213            Self::WhoisError { domain, message } => {
214                if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no match") {
215                    write!(f, "✅ {}: Domain appears to be available", domain)
216                } else if message.to_lowercase().contains("rate limit") || message.to_lowercase().contains("too many") {
217                    write!(f, "⏳ {}: WHOIS server is rate limiting requests\n   💡 Please wait a moment and try again", domain)
218                } else if message.to_lowercase().contains("whois") && message.to_lowercase().contains("not found") {
219                    write!(f, "⚠️ {}: WHOIS command not found on this system\n   💡 Please install whois or use online domain checkers", domain)
220                } else {
221                    write!(f, "⚠️ {}: WHOIS lookup failed\n   💡 This may indicate the domain is available or the server is busy", domain)
222                }
223            }
224            Self::BootstrapError { tld, message: _ } => {
225                write!(f, "❓ Unknown domain extension '.{}'\n   💡 This TLD may not support automated checking. Try manually checking with a registrar", tld)
226            }
227            Self::ParseError { message: _, content: _ } => {
228                write!(f, "⚠️ Unable to understand server response\n   💡 The domain registry may be experiencing issues. Please try again later")
229            }
230            Self::ConfigError { message } => {
231                write!(f, "⚙️ Configuration error: {}\n   💡 Please check your command line arguments or configuration file values", message)
232            }
233            Self::FileError { path, message } => {
234                if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no such file") {
235                    write!(f, "📁 File not found: {}\n   💡 Please check the file path and make sure the file exists", path)
236                } else if message.to_lowercase().contains("permission") {
237                    write!(f, "🔒 Permission denied: {}\n   💡 Please check file permissions or try running with appropriate access", path)
238                } else if message.to_lowercase().contains("no valid domains") {
239                    write!(f, "📄 No valid domains found in: {}\n   💡 Make sure the file contains domain names (one per line) and check the format", path)
240                } else {
241                    write!(f, "📁 File error ({}): {}\n   💡 Please check the file and try again", path, message)
242                }
243            }
244            Self::Timeout { operation, duration } => {
245                write!(f, "⏱️ Operation timed out after {:?}: {}\n   💡 Try reducing the number of domains or check your internet connection", duration, operation)
246            }
247            Self::RateLimited { service, message, retry_after } => {
248                match retry_after {
249                    Some(retry) => write!(f, "⏳ Rate limited by {}: {}\n   💡 Please wait {:?} and try again", service, message, retry),
250                    None => write!(f, "⏳ Rate limited by {}: {}\n   💡 Please wait a moment and try again", service, message),
251                }
252            }
253            Self::Internal { message } => {
254                write!(f, "🔧 Internal error: {}\n   💡 This is unexpected. Please try again or report this issue", message)
255            }
256        }
257    }
258}
259
260impl std::error::Error for DomainCheckError {}
261
262// Implement From conversions for common error types
263impl From<reqwest::Error> for DomainCheckError {
264    fn from(err: reqwest::Error) -> Self {
265        if err.is_timeout() {
266            Self::timeout("HTTP request", std::time::Duration::from_secs(30))
267        } else if err.is_connect() {
268            Self::network_with_source("Connection failed", err.to_string())
269        } else {
270            Self::network_with_source("HTTP request failed", err.to_string())
271        }
272    }
273}
274
275impl From<serde_json::Error> for DomainCheckError {
276    fn from(err: serde_json::Error) -> Self {
277        Self::ParseError {
278            message: format!("JSON parsing failed: {}", err),
279            content: None,
280        }
281    }
282}
283
284impl From<std::io::Error> for DomainCheckError {
285    fn from(err: std::io::Error) -> Self {
286        Self::Internal {
287            message: format!("I/O error: {}", err),
288        }
289    }
290}
291
292impl From<regex::Error> for DomainCheckError {
293    fn from(err: regex::Error) -> Self {
294        Self::Internal {
295            message: format!("Regex error: {}", err),
296        }
297    }
298}