lychee_lib/utils/
reqwest.rs1use std::error::Error;
2
3struct ErrorRule {
5 patterns: &'static [&'static str],
6 message: &'static str,
7}
8
9impl ErrorRule {
10 const fn new(patterns: &'static [&'static str], message: &'static str) -> Self {
12 Self { patterns, message }
13 }
14
15 fn matches(&self, text: &str) -> bool {
17 self.patterns.iter().any(|pattern| text.contains(pattern))
18 }
19
20 const fn message(&self) -> &'static str {
22 self.message
23 }
24}
25
26struct ErrorRules {
28 rules: Vec<ErrorRule>,
29 fallback: Option<String>,
30}
31
32impl ErrorRules {
33 const fn new() -> Self {
35 Self {
36 rules: Vec::new(),
37 fallback: None,
38 }
39 }
40
41 fn rule(mut self, patterns: &'static [&'static str], message: &'static str) -> Self {
43 self.rules.push(ErrorRule::new(patterns, message));
44 self
45 }
46
47 fn fallback(mut self, message: impl Into<String>) -> Self {
49 self.fallback = Some(message.into());
50 self
51 }
52
53 fn match_error(self, error_msg: &str) -> String {
55 for rule in &self.rules {
56 if rule.matches(error_msg) {
57 return rule.message().to_string();
58 }
59 }
60
61 self.fallback
62 .unwrap_or_else(|| format!("Unhandled error: {error_msg}"))
63 }
64}
65
66pub(crate) fn analyze_error_chain(error: &reqwest::Error) -> String {
77 if let Some(basic_message) = analyze_basic_reqwest_error(error) {
79 return basic_message;
80 }
81
82 if let Some(chain_message) = analyze_error_source_chain(error) {
84 return chain_message;
85 }
86
87 fallback_reqwest_analysis(error)
89}
90
91fn analyze_basic_reqwest_error(error: &reqwest::Error) -> Option<String> {
93 if error.is_timeout() {
94 return Some(
95 "Request timed out. Try increasing timeout or check server status".to_string(),
96 );
97 }
98
99 if error.is_redirect() {
100 return Some("Too many redirects - check for redirect loops".to_string());
101 }
102
103 if let Some(status) = error.status() {
104 let reason = status.canonical_reason().unwrap_or("Unknown");
105 return Some(format!(
106 "HTTP {status}: {reason} - check URL and server status",
107 ));
108 }
109
110 None
111}
112
113fn analyze_error_source_chain(error: &reqwest::Error) -> Option<String> {
115 let mut source = error.source();
116 while let Some(err) = source {
117 if let Some(io_error) = err.downcast_ref::<std::io::Error>() {
119 return Some(analyze_io_error(io_error));
120 }
121
122 if let Some(hyper_error) = err.downcast_ref::<hyper::Error>() {
124 return Some(analyze_hyper_error(hyper_error));
125 }
126
127 if let Some(url_error) = err.downcast_ref::<url::ParseError>() {
129 return Some(analyze_url_parse_error(*url_error));
130 }
131
132 if let Some(generic_message) = analyze_generic_error_string(&err.to_string()) {
134 return Some(generic_message);
135 }
136
137 source = err.source();
138 }
139
140 None
141}
142
143fn analyze_io_error(io_error: &std::io::Error) -> String {
145 match io_error.kind() {
146 std::io::ErrorKind::ConnectionRefused => {
147 "Connection refused - server may be down or port blocked".to_string()
148 }
149 std::io::ErrorKind::TimedOut => {
150 "Request timed out. Try increasing timeout or check server status".to_string()
151 }
152 std::io::ErrorKind::NotFound => {
153 "DNS resolution failed - check hostname spelling".to_string()
154 }
155 std::io::ErrorKind::PermissionDenied => {
156 "Permission denied - check firewall or proxy settings".to_string()
157 }
158 std::io::ErrorKind::Other => analyze_io_other_error(io_error),
159 std::io::ErrorKind::NetworkUnreachable => {
160 "Network unreachable. Check internet connection or VPN settings".to_string()
161 }
162 std::io::ErrorKind::AddrNotAvailable => {
163 "Address not available. Check network interface configuration".to_string()
164 }
165 std::io::ErrorKind::AddrInUse => {
166 "Address already in use. Port conflict or service already running".to_string()
167 }
168 std::io::ErrorKind::BrokenPipe => {
169 "Connection broken. Server closed connection unexpectedly".to_string()
170 }
171 std::io::ErrorKind::InvalidData => {
172 "Invalid response data. Server sent malformed response".to_string()
173 }
174 std::io::ErrorKind::UnexpectedEof => {
175 "Connection closed unexpectedly. Server terminated early".to_string()
176 }
177 std::io::ErrorKind::Interrupted => {
178 "Request interrupted. Try again or check for system issues".to_string()
179 }
180 std::io::ErrorKind::Unsupported => {
181 "Operation not supported. Check protocol or server capabilities".to_string()
182 }
183 _ => {
184 let kind_name = format!("{:?}", io_error.kind());
186 match kind_name.as_str() {
187 "Uncategorized" => {
188 "Connection failed. Check network connectivity and firewall settings"
189 .to_string()
190 }
191 _ => {
192 format!("I/O error ({kind_name}). Check network connectivity and server status",)
193 }
194 }
195 }
196 }
197}
198
199fn analyze_io_other_error(io_error: &std::io::Error) -> String {
201 if let Some(inner) = io_error.get_ref() {
202 let inner_msg = inner.to_string();
203
204 if inner_msg.contains("certificate") {
206 return analyze_certificate_error(&inner_msg);
207 }
208
209 ErrorRules::new()
211 .rule(
212 &["failed to lookup address", "nodename nor servname"],
213 "DNS resolution failed. Check hostname and DNS settings",
214 )
215 .rule(
216 &["Temporary failure in name resolution"],
217 "DNS temporarily unavailable. Try again later",
218 )
219 .rule(
220 &["handshake"],
221 "TLS handshake failed. Check SSL/TLS configuration",
222 )
223 .fallback(format!("Network error: {inner_msg}"))
224 .match_error(&inner_msg)
225 } else {
226 "Connection failed. Check network connectivity and firewall settings".to_string()
227 }
228}
229
230fn analyze_certificate_error(error_msg: &str) -> String {
232 ErrorRules::new()
233 .rule(
234 &[
235 "expired",
236 "NotValidAtThisTime",
237 "certificate has expired",
238 "certificate is not valid on",
239 ],
240 "SSL certificate expired. Site needs to renew certificate",
241 )
242 .rule(
243 &["hostname", "NotValidForName"],
244 "SSL certificate hostname mismatch. Check URL spelling",
245 )
246 .rule(
247 &["self signed", "UnknownIssuer", "not trusted"],
248 "SSL certificate not trusted. Use --insecure if site is trusted",
249 )
250 .rule(
251 &["verify failed"],
252 "SSL certificate verification failed. Check certificate validity",
253 )
254 .fallback("SSL certificate error. Check certificate validity")
255 .match_error(error_msg)
256}
257
258fn analyze_hyper_error(hyper_error: &hyper::Error) -> String {
260 if hyper_error.is_parse() {
261 if hyper_error.is_parse_status() {
262 return "Invalid HTTP status code from server".to_string();
263 }
264 return "Invalid HTTP response format. Server may be misconfigured".to_string();
265 }
266 if hyper_error.is_timeout() {
267 return "Request timed out. Try increasing timeout or check server status".to_string();
268 }
269 if hyper_error.is_user() {
270 if hyper_error.is_body_write_aborted() {
271 return "Request body upload was aborted".to_string();
272 }
273 return "Invalid request format. Check request parameters".to_string();
274 }
275 if hyper_error.is_canceled() {
276 return "Request was canceled".to_string();
277 }
278 if hyper_error.is_closed() {
279 return "Connection was closed unexpectedly".to_string();
280 }
281 if hyper_error.is_incomplete_message() {
282 return "Connection closed before response completed".to_string();
283 }
284
285 let hyper_msg = hyper_error.to_string();
286
287 ErrorRules::new()
289 .rule(
290 &["connection error"],
291 "Connection failed. Check network connectivity and firewall settings",
292 )
293 .rule(
294 &["http2 error"],
295 "HTTP/2 protocol error. Server may not support HTTP/2 properly",
296 )
297 .rule(
298 &["channel closed"],
299 "HTTP connection channel closed unexpectedly",
300 )
301 .rule(
302 &["operation was canceled"],
303 "HTTP operation was canceled before completion",
304 )
305 .rule(
306 &["message head is too large"],
307 "HTTP headers too large. Server response headers exceed limits",
308 )
309 .rule(
310 &["invalid content-length"],
311 "Invalid Content-Length header from server",
312 )
313 .fallback(format!("HTTP protocol error: {hyper_error}"))
314 .match_error(&hyper_msg)
315}
316
317fn analyze_url_parse_error(url_error: url::ParseError) -> String {
319 match url_error {
320 url::ParseError::EmptyHost => "Invalid URL: empty hostname".to_string(),
321 url::ParseError::InvalidDomainCharacter => {
322 "Invalid URL: invalid characters in domain".to_string()
323 }
324 url::ParseError::InvalidPort => "Invalid URL: invalid port number".to_string(),
325 url::ParseError::RelativeUrlWithoutBase => {
326 "Invalid URL: relative URL without base".to_string()
327 }
328 _ => format!("Invalid URL format: {url_error}"),
329 }
330}
331
332fn analyze_generic_error_string(error_msg: &str) -> Option<String> {
334 if error_msg.contains("certificate") {
336 return Some(analyze_certificate_error(error_msg));
337 }
338
339 if error_msg.contains("protocol") && error_msg.contains("not supported") {
341 return Some("Protocol not supported. Check URL scheme (http/https)".to_string());
342 }
343
344 let result = ErrorRules::new()
346 .rule(
347 &["protocol version"],
348 "TLS protocol version mismatch. The client and server cannot agree on a TLS version. This is often due to outdated system TLS libraries or the server TLS settings.",
349 )
350 .rule(
351 &["handshake", "TLS", "SSL"],
352 "TLS handshake failed. Check SSL/TLS configuration",
353 )
354 .rule(
355 &["name resolution", "hostname"],
356 "DNS resolution failed. Check hostname and DNS settings",
357 )
358 .rule(
359 &["Connection refused", "connection refused"],
360 "Connection refused. Server is not accepting connections (check if service is running)",
361 )
362 .rule(
363 &["Connection reset", "connection reset"],
364 "Connection reset by server. Server forcibly closed connection",
365 )
366 .rule(
367 &["No route to host", "no route"],
368 "No route to host. Check network routing or firewall configuration",
369 )
370 .rule(
371 &["Network is unreachable", "network unreachable"],
372 "Network unreachable. Check internet connection or VPN settings",
373 )
374 .rule(
375 &["timed out", "timeout"],
376 "Request timed out. Try increasing timeout or check server status",
377 )
378 .match_error(error_msg);
379
380 if result.starts_with("Unhandled error:") {
382 None
383 } else {
384 Some(result)
385 }
386}
387
388fn fallback_reqwest_analysis(error: &reqwest::Error) -> String {
390 if error.is_connect() {
391 "Connection failed. Check network connectivity and firewall settings".to_string()
392 } else if error.is_request() {
393 "Request failed. Check URL format and parameters".to_string()
394 } else if error.is_decode() {
395 "Response decoding failed. Server returned invalid data".to_string()
396 } else {
397 format!("Request failed: {error}")
398 }
399}