Skip to main content

imp_core/
error_display.rs

1pub 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
78        .find(|c: char| c == ':' || c == '\n' || c == '<')
79        .unwrap_or(rest.len());
80    let status = rest[..end].trim();
81    (!status.is_empty()).then(|| status.to_string())
82}
83
84fn extract_html_title(raw: &str) -> Option<String> {
85    let lower = raw.to_ascii_lowercase();
86    let title_start = lower.find("<title")?;
87    let open_end = lower[title_start..].find('>')? + title_start + 1;
88    let close_start = lower[open_end..].find("</title>")? + open_end;
89    let title = raw[open_end..close_start].trim();
90    (!title.is_empty()).then(|| title.to_string())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::format_error_for_display;
96
97    #[test]
98    fn extracts_nested_json_error_message() {
99        let raw = "Provider error: HTTP 401 Unauthorized: {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"OAuth token has expired\"}}";
100        assert_eq!(
101            format_error_for_display(raw),
102            "OAuth token has expired (use /login to refresh)"
103        );
104    }
105
106    #[test]
107    fn extracts_simple_json_message() {
108        let raw = "Provider error: HTTP 429 Too Many Requests: {\"message\":\"Rate limited\"}";
109        assert_eq!(format_error_for_display(raw), "Rate limited");
110    }
111
112    #[test]
113    fn collapses_html_error_pages_to_summary() {
114        let raw = "Provider error: HTTP 403 Forbidden: <!DOCTYPE html><html><head><title>Attention Required! | Cloudflare</title></head><body>blocked</body></html>";
115        assert_eq!(
116            format_error_for_display(raw),
117            "Provider returned an HTML error page (HTTP 403 Forbidden: Attention Required! | Cloudflare). This usually means an auth, gateway, proxy, or rate-limit issue."
118        );
119    }
120
121    #[test]
122    fn leaves_plain_text_errors_alone() {
123        let raw = "Provider error: connection reset by peer";
124        assert_eq!(format_error_for_display(raw), raw);
125    }
126
127    #[test]
128    fn replaces_empty_errors_with_unknown_error() {
129        assert_eq!(format_error_for_display("   \n"), "Unknown error.");
130    }
131}