Skip to main content

crispy_stream_checker/
error.rs

1//! Error types for stream checking operations.
2
3/// Errors that can occur during stream validation.
4#[derive(Debug, thiserror::Error)]
5pub enum CheckerError {
6    /// HTTP request failed.
7    #[error("HTTP error: {0}")]
8    Http(#[from] reqwest::Error),
9
10    /// Invalid URL provided.
11    #[error("invalid URL: {0}")]
12    InvalidUrl(String),
13
14    /// Request timed out.
15    #[error("request timed out after {timeout_ms}ms")]
16    Timeout { timeout_ms: u64 },
17
18    /// Connection refused by the server.
19    #[error("connection refused: {0}")]
20    ConnectionRefused(String),
21
22    /// I/O error (e.g., checkpoint file operations).
23    #[error("I/O error: {0}")]
24    Io(#[from] std::io::Error),
25}
26
27/// Summarize an error into a human-readable category.
28///
29/// Translated from IPTVChecker-Python `summarize_error()`:
30///
31/// ```python
32/// def summarize_error(exc):
33///     msg = str(exc).lower()
34///     if isinstance(exc, requests.Timeout):
35///         return "Connection timed out"
36///     if isinstance(exc, requests.ConnectionError):
37///         if any(kw in msg for kw in ['dns', ...]):
38///             return "DNS resolution failed"
39///         if 'ssl' in msg or 'tls' in msg or 'certificate' in msg or 'handshake' in msg:
40///             return "SSL/TLS error"
41///         if 'connection refused' in msg:
42///             return "Connection refused"
43///         return "Connection error"
44///     if isinstance(exc, requests.TooManyRedirects):
45///         return "Redirect loop"
46///     return str(exc)[:80]
47/// ```
48pub fn summarize_error(error: &reqwest::Error) -> String {
49    if error.is_timeout() {
50        return "Connection timed out".to_string();
51    }
52
53    if error.is_connect() {
54        let msg = error.to_string().to_lowercase();
55
56        if msg.contains("dns")
57            || msg.contains("name or service not known")
58            || msg.contains("nodename nor servname")
59            || msg.contains("no such host")
60            || msg.contains("getaddrinfo failed")
61        {
62            return "DNS resolution failed".to_string();
63        }
64
65        if msg.contains("ssl")
66            || msg.contains("tls")
67            || msg.contains("certificate")
68            || msg.contains("handshake")
69        {
70            return "SSL/TLS error".to_string();
71        }
72
73        if msg.contains("connection refused") {
74            return "Connection refused".to_string();
75        }
76
77        return "Connection error".to_string();
78    }
79
80    if error.is_redirect() {
81        return "Redirect loop".to_string();
82    }
83
84    // Truncate to 80 chars, matching Python's `str(exc)[:80]`.
85    let msg = error.to_string();
86    if msg.len() > 80 {
87        format!("{}...", &msg[..77])
88    } else {
89        msg
90    }
91}
92
93/// Summarize a generic error string into a human-readable category.
94///
95/// Works on pre-formatted error messages (not `reqwest::Error` instances).
96pub fn summarize_error_str(msg: &str) -> &'static str {
97    let lower = msg.to_lowercase();
98
99    if lower.contains("timed out") || lower.contains("timeout") {
100        return "Connection timed out";
101    }
102    if lower.contains("dns")
103        || lower.contains("name or service not known")
104        || lower.contains("getaddrinfo")
105    {
106        return "DNS resolution failed";
107    }
108    if lower.contains("ssl") || lower.contains("tls") || lower.contains("certificate") {
109        return "SSL/TLS error";
110    }
111    if lower.contains("connection refused") {
112        return "Connection refused";
113    }
114    if lower.contains("redirect") {
115        return "Redirect loop";
116    }
117    "Connection error"
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn summarize_timeout_string() {
126        assert_eq!(
127            summarize_error_str("request timed out after 10000ms"),
128            "Connection timed out"
129        );
130    }
131
132    #[test]
133    fn summarize_dns_string() {
134        assert_eq!(
135            summarize_error_str("DNS resolution failed for host"),
136            "DNS resolution failed"
137        );
138    }
139
140    #[test]
141    fn summarize_ssl_string() {
142        assert_eq!(summarize_error_str("SSL handshake error"), "SSL/TLS error");
143    }
144
145    #[test]
146    fn summarize_refused_string() {
147        assert_eq!(
148            summarize_error_str("Connection refused on port 8080"),
149            "Connection refused"
150        );
151    }
152
153    #[test]
154    fn summarize_redirect_string() {
155        assert_eq!(summarize_error_str("too many redirects"), "Redirect loop");
156    }
157
158    #[test]
159    fn summarize_unknown_string() {
160        assert_eq!(
161            summarize_error_str("some other failure"),
162            "Connection error"
163        );
164    }
165}