Skip to main content

turbo_cdn/
error.rs

1// Licensed under the MIT License
2// Copyright (c) 2025 Hal <hal.long@outlook.com>
3
4use std::fmt;
5use thiserror::Error;
6
7/// Main error type for turbo-cdn operations
8#[derive(Error, Debug)]
9pub enum TurboCdnError {
10    /// Network-related errors
11    #[error("Network error: {0}")]
12    Network(#[from] reqwest::Error),
13
14    /// IO-related errors
15    #[error("IO error: {0}")]
16    Io(#[from] std::io::Error),
17
18    /// URL parsing errors
19    #[error("Invalid URL: {0}")]
20    InvalidUrl(#[from] url::ParseError),
21
22    /// JSON parsing errors
23    #[error("JSON parsing error: {0}")]
24    Json(#[from] serde_json::Error),
25
26    /// Configuration errors
27    #[error("Configuration error: {message}")]
28    Config { message: String },
29
30    /// Download errors
31    #[error("Download failed: {message}")]
32    Download { message: String },
33
34    /// Source validation errors
35    #[error("Source validation failed: {message}")]
36    SourceValidation { message: String },
37
38    /// Compliance errors
39    #[error("Compliance check failed: {message}")]
40    Compliance { message: String },
41
42    /// Cache errors
43    #[error("Cache error: {message}")]
44    Cache { message: String },
45
46    /// Routing errors
47    #[error("Routing error: {message}")]
48    Routing { message: String },
49
50    /// Authentication errors
51    #[error("Authentication failed: {message}")]
52    Authentication { message: String },
53
54    /// Rate limiting errors
55    #[error("Rate limit exceeded: {message}")]
56    RateLimit { message: String },
57
58    /// Timeout errors
59    #[error("Operation timed out: {message}")]
60    Timeout { message: String },
61
62    /// Checksum validation errors
63    #[error("Checksum validation failed: expected {expected}, got {actual}")]
64    ChecksumMismatch { expected: String, actual: String },
65
66    /// File not found errors
67    #[error("File not found: {path}")]
68    FileNotFound { path: String },
69
70    /// HTTP status code errors (non-retryable client errors like 404)
71    #[error("HTTP {status_code}: {message}")]
72    HttpStatus {
73        status_code: u16,
74        message: String,
75        url: String,
76    },
77
78    /// Server errors (5xx, potentially retryable)
79    #[error("Server error {status_code}: {message}")]
80    ServerError {
81        status_code: u16,
82        message: String,
83        url: String,
84    },
85
86    /// Unsupported operation errors
87    #[error("Unsupported operation: {message}")]
88    Unsupported { message: String },
89
90    /// Generic errors
91    #[error("Internal error: {message}")]
92    Internal { message: String },
93}
94
95impl TurboCdnError {
96    /// Create a new configuration error
97    pub fn config<S: Into<String>>(message: S) -> Self {
98        Self::Config {
99            message: message.into(),
100        }
101    }
102
103    /// Create a new download error
104    pub fn download<S: Into<String>>(message: S) -> Self {
105        Self::Download {
106            message: message.into(),
107        }
108    }
109
110    /// Create a new source validation error
111    pub fn source_validation<S: Into<String>>(message: S) -> Self {
112        Self::SourceValidation {
113            message: message.into(),
114        }
115    }
116
117    /// Create a new compliance error
118    pub fn compliance<S: Into<String>>(message: S) -> Self {
119        Self::Compliance {
120            message: message.into(),
121        }
122    }
123
124    /// Create a new cache error
125    pub fn cache<S: Into<String>>(message: S) -> Self {
126        Self::Cache {
127            message: message.into(),
128        }
129    }
130
131    /// Create a new routing error
132    pub fn routing<S: Into<String>>(message: S) -> Self {
133        Self::Routing {
134            message: message.into(),
135        }
136    }
137
138    /// Create a new authentication error
139    pub fn authentication<S: Into<String>>(message: S) -> Self {
140        Self::Authentication {
141            message: message.into(),
142        }
143    }
144
145    /// Create a new rate limit error
146    pub fn rate_limit<S: Into<String>>(message: S) -> Self {
147        Self::RateLimit {
148            message: message.into(),
149        }
150    }
151
152    /// Create a new timeout error
153    pub fn timeout<S: Into<String>>(message: S) -> Self {
154        Self::Timeout {
155            message: message.into(),
156        }
157    }
158
159    /// Create a new checksum mismatch error
160    pub fn checksum_mismatch<S: Into<String>>(expected: S, actual: S) -> Self {
161        Self::ChecksumMismatch {
162            expected: expected.into(),
163            actual: actual.into(),
164        }
165    }
166
167    /// Create a new file not found error
168    pub fn file_not_found<S: Into<String>>(path: S) -> Self {
169        Self::FileNotFound { path: path.into() }
170    }
171
172    /// Create a new HTTP status error (for 4xx client errors)
173    pub fn http_status<S: Into<String>>(status_code: u16, message: S, url: S) -> Self {
174        Self::HttpStatus {
175            status_code,
176            message: message.into(),
177            url: url.into(),
178        }
179    }
180
181    /// Create a new server error (for 5xx errors)
182    pub fn server_error<S: Into<String>>(status_code: u16, message: S, url: S) -> Self {
183        Self::ServerError {
184            status_code,
185            message: message.into(),
186            url: url.into(),
187        }
188    }
189
190    /// Create an error from HTTP status code
191    pub fn from_status_code<S: Into<String>>(status_code: u16, url: S) -> Self {
192        let url_str = url.into();
193        let message = match status_code {
194            400 => "Bad Request",
195            401 => "Unauthorized",
196            403 => "Forbidden",
197            404 => "Not Found",
198            405 => "Method Not Allowed",
199            408 => "Request Timeout",
200            429 => "Too Many Requests",
201            500 => "Internal Server Error",
202            502 => "Bad Gateway",
203            503 => "Service Unavailable",
204            504 => "Gateway Timeout",
205            _ => "Unknown Error",
206        };
207
208        if status_code >= 500 {
209            Self::ServerError {
210                status_code,
211                message: message.to_string(),
212                url: url_str,
213            }
214        } else if status_code == 429 {
215            Self::RateLimit {
216                message: format!("Rate limited by {url_str}"),
217            }
218        } else {
219            Self::HttpStatus {
220                status_code,
221                message: message.to_string(),
222                url: url_str,
223            }
224        }
225    }
226
227    /// Create a new unsupported operation error
228    pub fn unsupported<S: Into<String>>(message: S) -> Self {
229        Self::Unsupported {
230            message: message.into(),
231        }
232    }
233
234    /// Create a new internal error
235    pub fn internal<S: Into<String>>(message: S) -> Self {
236        Self::Internal {
237            message: message.into(),
238        }
239    }
240
241    /// Create a new network error
242    pub fn network<S: Into<String>>(message: S) -> Self {
243        let msg = message.into();
244        Self::Internal {
245            message: format!("Network error: {msg}"),
246        }
247    }
248
249    /// Create a new IO error
250    pub fn io<S: Into<String>>(message: S) -> Self {
251        let msg = message.into();
252        Self::Internal {
253            message: format!("IO error: {msg}"),
254        }
255    }
256
257    /// Check if the error is retryable
258    pub fn is_retryable(&self) -> bool {
259        match self {
260            // Network errors are generally retryable
261            TurboCdnError::Network(_) => true,
262            // Timeouts are retryable
263            TurboCdnError::Timeout { .. } => true,
264            // Rate limits should be retried after delay
265            TurboCdnError::RateLimit { .. } => true,
266            // IO errors might be transient
267            TurboCdnError::Io(_) => true,
268            // Server errors (5xx) are retryable
269            TurboCdnError::ServerError { .. } => true,
270            // HTTP client errors (4xx) are NOT retryable - the resource doesn't exist
271            TurboCdnError::HttpStatus { .. } => false,
272            // All other errors are not retryable
273            _ => false,
274        }
275    }
276
277    /// Check if this error indicates the resource doesn't exist on this server
278    /// and we should immediately try the next mirror
279    pub fn should_try_next_mirror(&self) -> bool {
280        match self {
281            // 404 Not Found - definitely try next mirror
282            TurboCdnError::HttpStatus { status_code, .. } => *status_code == 404,
283            // Server errors might be temporary, but try next mirror anyway
284            TurboCdnError::ServerError { .. } => true,
285            // Network errors - try next mirror
286            TurboCdnError::Network(_) => true,
287            // Timeout - try next mirror
288            TurboCdnError::Timeout { .. } => true,
289            _ => false,
290        }
291    }
292
293    /// Get HTTP status code if this is an HTTP error
294    pub fn status_code(&self) -> Option<u16> {
295        match self {
296            TurboCdnError::HttpStatus { status_code, .. } => Some(*status_code),
297            TurboCdnError::ServerError { status_code, .. } => Some(*status_code),
298            _ => None,
299        }
300    }
301
302    /// Get the error category for metrics and logging
303    pub fn category(&self) -> &'static str {
304        match self {
305            TurboCdnError::Network(_) => "network",
306            TurboCdnError::Io(_) => "io",
307            TurboCdnError::InvalidUrl(_) => "url",
308            TurboCdnError::Json(_) => "json",
309            TurboCdnError::Config { .. } => "config",
310            TurboCdnError::Download { .. } => "download",
311            TurboCdnError::SourceValidation { .. } => "source_validation",
312            TurboCdnError::Compliance { .. } => "compliance",
313            TurboCdnError::Cache { .. } => "cache",
314            TurboCdnError::Routing { .. } => "routing",
315            TurboCdnError::Authentication { .. } => "authentication",
316            TurboCdnError::RateLimit { .. } => "rate_limit",
317            TurboCdnError::Timeout { .. } => "timeout",
318            TurboCdnError::ChecksumMismatch { .. } => "checksum",
319            TurboCdnError::FileNotFound { .. } => "file_not_found",
320            TurboCdnError::HttpStatus { .. } => "http_status",
321            TurboCdnError::ServerError { .. } => "server_error",
322            TurboCdnError::Unsupported { .. } => "unsupported",
323            TurboCdnError::Internal { .. } => "internal",
324        }
325    }
326}
327
328/// Result type alias for turbo-cdn operations
329pub type Result<T> = std::result::Result<T, TurboCdnError>;
330
331/// Error context for better error reporting
332#[derive(Debug, Clone)]
333pub struct ErrorContext {
334    pub operation: String,
335    pub source: Option<String>,
336    pub file_path: Option<String>,
337    pub url: Option<String>,
338    pub timestamp: chrono::DateTime<chrono::Utc>,
339}
340
341impl ErrorContext {
342    pub fn new(operation: impl Into<String>) -> Self {
343        Self {
344            operation: operation.into(),
345            source: None,
346            file_path: None,
347            url: None,
348            timestamp: chrono::Utc::now(),
349        }
350    }
351
352    pub fn with_source(mut self, source: impl Into<String>) -> Self {
353        self.source = Some(source.into());
354        self
355    }
356
357    pub fn with_file_path(mut self, path: impl Into<String>) -> Self {
358        self.file_path = Some(path.into());
359        self
360    }
361
362    pub fn with_url(mut self, url: impl Into<String>) -> Self {
363        self.url = Some(url.into());
364        self
365    }
366}
367
368impl fmt::Display for ErrorContext {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        let operation = &self.operation;
371        write!(f, "Operation: {operation}")?;
372        if let Some(source) = &self.source {
373            write!(f, ", Source: {source}")?;
374        }
375        if let Some(path) = &self.file_path {
376            write!(f, ", File: {path}")?;
377        }
378        if let Some(url) = &self.url {
379            write!(f, ", URL: {url}")?;
380        }
381        let timestamp = self.timestamp.format("%Y-%m-%d %H:%M:%S UTC");
382        write!(f, ", Time: {timestamp}")
383    }
384}