url_preview/
error.rs

1use thiserror::Error;
2#[cfg(feature = "logging")]
3use tracing::{error, warn};
4
5#[derive(Debug, Error)]
6pub enum PreviewError {
7    #[error("Failed to parse URL: {0}")]
8    UrlParseError(#[from] url::ParseError),
9
10    #[error("Failed to fetch content: {0}")]
11    FetchError(String),
12
13    #[error("Failed to extract metadata: {0}")]
14    ExtractError(String),
15
16    #[error("Cache error: {0}")]
17    CacheError(String),
18
19    #[error("Rate limit exceeded: {0}")]
20    RateLimitError(String),
21
22    #[error("Invalid content type: {0}")]
23    InvalidContentType(String),
24
25    #[error("Request timeout: {0}")]
26    TimeoutError(String),
27
28    #[error("DNS resolution failed: {0}")]
29    DnsError(String),
30
31    #[error("Connection error: {0}")]
32    ConnectionError(String),
33
34    #[error("HTTP {status}: {message}")]
35    HttpError { status: u16, message: String },
36
37    #[error("Server error (5xx): {status} - {message}")]
38    ServerError { status: u16, message: String },
39
40    #[error("Client error (4xx): {status} - {message}")]
41    ClientError { status: u16, message: String },
42
43    #[error("External service error: {service} - {message}")]
44    ExternalServiceError { service: String, message: String },
45
46    #[error("Parse error: {0}")]
47    ParseError(String),
48
49    #[error("Concurrency limit reached")]
50    ConcurrencyLimitError,
51
52    #[error("Resource not found: {0}")]
53    NotFound(String),
54
55    #[error("Invalid URL scheme: {0}")]
56    InvalidUrlScheme(String),
57
58    #[error("Invalid URL: {0}")]
59    InvalidUrl(String),
60
61    #[error("Domain not in allowed list: {0}")]
62    DomainNotAllowed(String),
63
64    #[error("Domain is blocked: {0}")]
65    DomainBlocked(String),
66
67    #[error("Localhost URLs are not allowed")]
68    LocalhostBlocked,
69
70    #[error("Private IP address blocked: {0}")]
71    PrivateIpBlocked(String),
72
73    #[error("Content size exceeds limit: {size} > {limit}")]
74    ContentSizeExceeded { size: usize, limit: usize },
75
76    #[error("Download time exceeded: {elapsed}s > {limit}s")]
77    DownloadTimeExceeded { elapsed: u64, limit: u64 },
78
79    #[error("Content type not allowed: {0}")]
80    ContentTypeNotAllowed(String),
81    
82    #[error("Unsupported operation: {0}")]
83    UnsupportedOperation(String),
84    
85    #[error("Configuration error: {0}")]
86    InvalidConfiguration(String),
87    
88    #[error("JSON parsing error: {0}")]
89    JsonError(#[from] serde_json::Error),
90}
91
92impl PreviewError {
93    pub fn log(&self) {
94        #[cfg(feature = "logging")]
95        match self {
96            PreviewError::UrlParseError(e) => {
97                warn!(error = %e, "URL parsing failed");
98            }
99            PreviewError::FetchError(e) => {
100                error!(error = %e, "Content fetch failed");
101            }
102            PreviewError::ExtractError(e) => {
103                error!(error = %e, "Metadata extraction failed");
104            }
105            PreviewError::CacheError(e) => {
106                warn!(error = %e, "Cache operation failed");
107            }
108            PreviewError::RateLimitError(e) => {
109                warn!(error = %e, "Rate limit exceeded");
110            }
111            PreviewError::InvalidContentType(e) => {
112                warn!(error = %e, "Invalid content type received");
113            }
114            PreviewError::TimeoutError(e) => {
115                warn!(error = %e, "Request timed out");
116            }
117            PreviewError::ExternalServiceError { service, message } => {
118                error!(
119                    service = %service,
120                    error = %message,
121                    "External service error occurred"
122                );
123            }
124            PreviewError::ParseError(e) => {
125                error!(error = %e, "Parse error occurred");
126            }
127            PreviewError::ConcurrencyLimitError => {
128                warn!("Concurrency limit reached");
129            }
130            PreviewError::NotFound(e) => {
131                warn!(error = %e, "Resource not found");
132            }
133            PreviewError::DnsError(e) => {
134                error!(error = %e, "DNS resolution failed");
135            }
136            PreviewError::ConnectionError(e) => {
137                error!(error = %e, "Connection failed");
138            }
139            PreviewError::HttpError { status, message } => {
140                warn!(status = %status, error = %message, "HTTP error");
141            }
142            PreviewError::ServerError { status, message } => {
143                error!(status = %status, error = %message, "Server error");
144            }
145            PreviewError::ClientError { status, message } => {
146                warn!(status = %status, error = %message, "Client error");
147            }
148            PreviewError::InvalidUrlScheme(scheme) => {
149                warn!(scheme = %scheme, "Invalid URL scheme");
150            }
151            PreviewError::InvalidUrl(e) => {
152                warn!(error = %e, "Invalid URL");
153            }
154            PreviewError::DomainNotAllowed(domain) => {
155                warn!(domain = %domain, "Domain not in allowed list");
156            }
157            PreviewError::DomainBlocked(domain) => {
158                warn!(domain = %domain, "Domain is blocked");
159            }
160            PreviewError::LocalhostBlocked => {
161                warn!("Localhost URL blocked");
162            }
163            PreviewError::PrivateIpBlocked(ip) => {
164                warn!(ip = %ip, "Private IP address blocked");
165            }
166            PreviewError::ContentSizeExceeded { size, limit } => {
167                warn!(size = %size, limit = %limit, "Content size exceeded");
168            }
169            PreviewError::DownloadTimeExceeded { elapsed, limit } => {
170                warn!(elapsed = %elapsed, limit = %limit, "Download time exceeded");
171            }
172            PreviewError::ContentTypeNotAllowed(content_type) => {
173                warn!(content_type = %content_type, "Content type not allowed");
174            }
175            PreviewError::UnsupportedOperation(op) => {
176                warn!(operation = %op, "Unsupported operation");
177            }
178            PreviewError::JsonError(e) => {
179                error!(error = %e, "JSON parsing error");
180            }
181        }
182        #[cfg(not(feature = "logging"))]
183        {
184            // No-op when logging is disabled
185        }
186    }
187
188    /// Convert a reqwest error into a more specific PreviewError
189    pub fn from_reqwest_error(error: reqwest::Error) -> Self {
190        if error.is_timeout() {
191            PreviewError::TimeoutError(error.to_string())
192        } else if error.is_connect() {
193            // Connection errors including DNS
194            let error_msg = error.to_string();
195            if error_msg.contains("dns")
196                || error_msg.contains("resolve")
197                || error_msg.contains("lookup")
198            {
199                PreviewError::DnsError(error_msg)
200            } else {
201                PreviewError::ConnectionError(error_msg)
202            }
203        } else if let Some(status) = error.status() {
204            let status_code = status.as_u16();
205            let message = error.to_string();
206
207            match status_code {
208                404 => PreviewError::NotFound(message),
209                400..=499 => PreviewError::ClientError {
210                    status: status_code,
211                    message,
212                },
213                500..=599 => PreviewError::ServerError {
214                    status: status_code,
215                    message,
216                },
217                _ => PreviewError::HttpError {
218                    status: status_code,
219                    message,
220                },
221            }
222        } else {
223            // Generic fetch error for other cases
224            PreviewError::FetchError(error.to_string())
225        }
226    }
227}