Skip to main content

har/
errorbody.rs

1use serde_json::Value;
2
3#[derive(Debug, Default)]
4pub struct ErrorFields {
5    pub message: Option<String>,
6    pub code: Option<String>,
7}
8
9const MESSAGE_KEYS: &[&str] = &[
10    "message",
11    "error_description",
12    "error",
13    "reason",
14    "detail",
15    "details",
16];
17const CODE_KEYS: &[&str] = &["code", "error_code", "status"];
18
19/// Extract common error fields from a JSON response body. Returns empty fields
20/// for non-JSON or when keys are absent.
21pub fn parse_error_fields(body: &str) -> ErrorFields {
22    let mut fields = ErrorFields::default();
23    let Ok(v) = serde_json::from_str::<Value>(body) else {
24        return fields;
25    };
26    fields.message = first_string(&v, MESSAGE_KEYS);
27    fields.code = first_string(&v, CODE_KEYS);
28    fields
29}
30
31fn first_string(v: &Value, keys: &[&str]) -> Option<String> {
32    let obj = v.as_object()?;
33    for k in keys {
34        match obj.get(*k) {
35            Some(Value::String(s)) => return Some(s.clone()),
36            Some(Value::Number(n)) => return Some(n.to_string()),
37            _ => {}
38        }
39    }
40    // Fall back to a nested "error" object.
41    if let Some(Value::Object(err)) = obj.get("error") {
42        for k in keys {
43            match err.get(*k) {
44                Some(Value::String(s)) => return Some(s.clone()),
45                Some(Value::Number(n)) => return Some(n.to_string()),
46                _ => {}
47            }
48        }
49    }
50    None
51}
52
53#[cfg(test)]
54mod tests {
55    use super::parse_error_fields;
56
57    #[test]
58    fn extracts_message_and_code_flat() {
59        let f = parse_error_fields(r#"{"message":"Not found","code":"NF404"}"#);
60        assert_eq!(f.message.as_deref(), Some("Not found"));
61        assert_eq!(f.code.as_deref(), Some("NF404"));
62    }
63
64    #[test]
65    fn extracts_from_nested_error_object() {
66        let f = parse_error_fields(r#"{"error":{"message":"bad token","code":"E1"}}"#);
67        assert_eq!(f.message.as_deref(), Some("bad token"));
68        assert_eq!(f.code.as_deref(), Some("E1"));
69    }
70
71    #[test]
72    fn numeric_code_becomes_string() {
73        let f = parse_error_fields(r#"{"error":"boom","status":500}"#);
74        assert_eq!(f.message.as_deref(), Some("boom"));
75        assert_eq!(f.code.as_deref(), Some("500"));
76    }
77
78    #[test]
79    fn non_json_is_empty() {
80        let f = parse_error_fields("<html>500</html>");
81        assert!(f.message.is_none());
82        assert!(f.code.is_none());
83    }
84}