Skip to main content

ironbeam_rs/
error.rs

1/// Parsed API error response body.
2///
3/// Some endpoints (e.g. 429 rate-limit) nest the error inside a `result` object.
4#[derive(Debug, serde::Deserialize)]
5struct ApiErrorBody {
6    error1: Option<String>,
7    message: Option<String>,
8    result: Option<Box<ApiErrorBody>>,
9}
10
11/// Extract a human-readable message from an API error JSON body.
12/// Checks top-level `error1`/`message` first, then nested `result`, then raw body.
13pub(crate) fn parse_api_error(body: &[u8]) -> String {
14    if let Ok(parsed) = serde_json::from_slice::<ApiErrorBody>(body)
15        && let Some(msg) = extract_message(&parsed, 3)
16    {
17        return msg;
18    }
19    String::from_utf8_lossy(body).into_owned()
20}
21
22fn extract_message(body: &ApiErrorBody, max_depth: u8) -> Option<String> {
23    if let Some(e) = body.error1.as_ref().filter(|s| !s.is_empty()) {
24        return Some(e.clone());
25    }
26    if let Some(m) = body.message.as_ref().filter(|s| !s.is_empty()) {
27        return Some(m.clone());
28    }
29    if max_depth > 0
30        && let Some(inner) = &body.result
31    {
32        return extract_message(inner, max_depth - 1);
33    }
34    None
35}
36
37/// Crate-level error type.
38#[derive(Debug, thiserror::Error)]
39pub enum Error {
40    #[error("http: {0}")]
41    Http(#[from] hyper::Error),
42
43    #[error("http: {0}")]
44    HttpClient(#[from] hyper_util::client::legacy::Error),
45
46    #[error("json: {0}")]
47    Json(#[from] serde_json::Error),
48
49    #[error("api error {status}: {message}")]
50    Api { status: u16, message: String },
51
52    #[error("auth failed: {0}")]
53    Auth(String),
54
55    #[error("invalid uri: {0}")]
56    InvalidUri(#[from] hyper::http::uri::InvalidUri),
57
58    #[error("websocket: {0}")]
59    WebSocket(String),
60
61    #[error("{0}")]
62    Other(String),
63}
64
65/// Crate-level Result alias.
66pub type Result<T> = std::result::Result<T, Error>;
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn parse_api_error_prefers_error1() {
74        let body = br#"{"error1":"Unauthorized","message":"bad creds"}"#;
75        assert_eq!(parse_api_error(body), "Unauthorized");
76    }
77
78    #[test]
79    fn parse_api_error_falls_back_to_message() {
80        let body = br#"{"message":"something went wrong"}"#;
81        assert_eq!(parse_api_error(body), "something went wrong");
82    }
83
84    #[test]
85    fn parse_api_error_skips_empty_error1() {
86        let body = br#"{"error1":"","message":"fallback"}"#;
87        assert_eq!(parse_api_error(body), "fallback");
88    }
89
90    #[test]
91    fn parse_api_error_raw_body_on_invalid_json() {
92        let body = b"not json at all";
93        assert_eq!(parse_api_error(body), "not json at all");
94    }
95
96    #[test]
97    fn parse_api_error_raw_body_when_no_fields() {
98        let body = br#"{"other":"field"}"#;
99        assert_eq!(parse_api_error(body), r#"{"other":"field"}"#);
100    }
101
102    #[test]
103    fn parse_api_error_nested_result() {
104        let body = br#"{"result":{"additionalProperties":{},"error1":"Excessive calls in the last second - maximum allowed is 10","status":1,"message":"Error"},"statusCode":429,"headers":{}}"#;
105        assert_eq!(
106            parse_api_error(body),
107            "Excessive calls in the last second - maximum allowed is 10"
108        );
109    }
110}