1#[derive(Debug, serde::Deserialize)]
5struct ApiErrorBody {
6 error1: Option<String>,
7 message: Option<String>,
8 result: Option<Box<ApiErrorBody>>,
9}
10
11pub(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#[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
65pub 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}