imp_core/
error_display.rs1pub fn format_error_for_display(raw: &str) -> String {
2 let trimmed = raw.trim();
3 if trimmed.is_empty() {
4 return "Unknown error.".to_string();
5 }
6
7 if let Some(message) = extract_json_message(trimmed) {
8 return with_auth_hint(&message);
9 }
10
11 if looks_like_html_error(trimmed) {
12 let status = extract_http_status(trimmed);
13 let title = extract_html_title(trimmed);
14
15 return match (status, title) {
16 (Some(status), Some(title)) => format!(
17 "Provider returned an HTML error page ({status}: {title}). This usually means an auth, gateway, proxy, or rate-limit issue."
18 ),
19 (Some(status), None) => format!(
20 "Provider returned an HTML error page ({status}). This usually means an auth, gateway, proxy, or rate-limit issue."
21 ),
22 (None, Some(title)) => format!(
23 "Provider returned an HTML error page ({title}). This usually means an auth, gateway, proxy, or rate-limit issue."
24 ),
25 (None, None) => "Provider returned an HTML error page. This usually means an auth, gateway, proxy, or rate-limit issue.".to_string(),
26 };
27 }
28
29 trimmed.to_string()
30}
31
32fn extract_json_message(raw: &str) -> Option<String> {
33 let json_start = raw.find('{')?;
34 let parsed = serde_json::from_str::<serde_json::Value>(&raw[json_start..]).ok()?;
35
36 parsed
37 .get("error")
38 .and_then(|error| error.get("message"))
39 .and_then(|message| message.as_str())
40 .map(ToOwned::to_owned)
41 .or_else(|| {
42 parsed
43 .get("message")
44 .and_then(|message| message.as_str())
45 .map(ToOwned::to_owned)
46 })
47}
48
49fn with_auth_hint(message: &str) -> String {
50 let lower = message.to_ascii_lowercase();
51 let needs_login_hint = lower.contains("expired")
52 || lower.contains("oauth")
53 || (lower.contains("token")
54 && (lower.contains("expired")
55 || lower.contains("invalid")
56 || lower.contains("refresh")));
57
58 if needs_login_hint {
59 format!("{message} (use /login to refresh)")
60 } else {
61 message.to_string()
62 }
63}
64
65fn looks_like_html_error(raw: &str) -> bool {
66 let lower = raw.to_ascii_lowercase();
67 lower.contains("<!doctype html")
68 || lower.contains("<html")
69 || lower.contains("<head")
70 || lower.contains("<body")
71 || lower.contains("<title")
72}
73
74fn extract_http_status(raw: &str) -> Option<String> {
75 let start = raw.find("HTTP ")?;
76 let rest = &raw[start..];
77 let end = rest.find([':', '\n', '<']).unwrap_or(rest.len());
78 let status = rest[..end].trim();
79 (!status.is_empty()).then(|| status.to_string())
80}
81
82fn extract_html_title(raw: &str) -> Option<String> {
83 let lower = raw.to_ascii_lowercase();
84 let title_start = lower.find("<title")?;
85 let open_end = lower[title_start..].find('>')? + title_start + 1;
86 let close_start = lower[open_end..].find("</title>")? + open_end;
87 let title = raw[open_end..close_start].trim();
88 (!title.is_empty()).then(|| title.to_string())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::format_error_for_display;
94
95 #[test]
96 fn extracts_nested_json_error_message() {
97 let raw = "Provider error: HTTP 401 Unauthorized: {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"OAuth token has expired\"}}";
98 assert_eq!(
99 format_error_for_display(raw),
100 "OAuth token has expired (use /login to refresh)"
101 );
102 }
103
104 #[test]
105 fn extracts_simple_json_message() {
106 let raw = "Provider error: HTTP 429 Too Many Requests: {\"message\":\"Rate limited\"}";
107 assert_eq!(format_error_for_display(raw), "Rate limited");
108 }
109
110 #[test]
111 fn collapses_html_error_pages_to_summary() {
112 let raw = "Provider error: HTTP 403 Forbidden: <!DOCTYPE html><html><head><title>Attention Required! | Cloudflare</title></head><body>blocked</body></html>";
113 assert_eq!(
114 format_error_for_display(raw),
115 "Provider returned an HTML error page (HTTP 403 Forbidden: Attention Required! | Cloudflare). This usually means an auth, gateway, proxy, or rate-limit issue."
116 );
117 }
118
119 #[test]
120 fn leaves_plain_text_errors_alone() {
121 let raw = "Provider error: connection reset by peer";
122 assert_eq!(format_error_for_display(raw), raw);
123 }
124
125 #[test]
126 fn replaces_empty_errors_with_unknown_error() {
127 assert_eq!(format_error_for_display(" \n"), "Unknown error.");
128 }
129}