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 }
186 }
187
188 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 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 PreviewError::FetchError(error.to_string())
225 }
226 }
227}