1use std::fmt;
2
3use reqwest::header::HeaderMap;
4use serde_json::Value;
5
6use crate::JsonObject;
7
8#[derive(Debug, thiserror::Error)]
10pub enum Error {
11 #[error(transparent)]
13 Api(#[from] ApiError),
14 #[error("transport error: {0}")]
16 Http(#[from] reqwest::Error),
17 #[error("invalid JSON response: {0}")]
19 Json(#[from] serde_json::Error),
20 #[error("{0}")]
23 RunFailed(String),
24 #[error("timeout waiting for run result")]
26 Timeout,
27 #[error("{0}")]
29 Protocol(String),
30}
31
32#[derive(Debug, Clone)]
34pub struct ApiError {
35 pub status_code: u16,
36 pub message: String,
37 pub code: Option<String>,
38 pub request_id: Option<String>,
39 pub retry_after: Option<u64>,
42 pub error: Option<JsonObject>,
44}
45
46impl fmt::Display for ApiError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{} (status {})", self.message, self.status_code)
49 }
50}
51
52impl std::error::Error for ApiError {}
53
54pub(crate) fn parse_api_error(status_code: u16, text: &str, headers: &HeaderMap) -> ApiError {
55 let parsed: Value = serde_json::from_str(text).unwrap_or(Value::Null);
56 let error_object = parsed.get("error").and_then(Value::as_object);
57
58 match error_object {
59 Some(error) => {
60 let message = error
61 .get("message")
62 .and_then(Value::as_str)
63 .map(str::to_string)
64 .unwrap_or_else(|| format!("Hot API error ({status_code})"));
65 ApiError {
66 status_code,
67 message,
68 code: stringified(error.get("code")),
69 request_id: stringified(error.get("request_id")),
70 retry_after: retry_after(error.get("retry_after"), headers),
71 error: Some(error.clone()),
72 }
73 }
74 None => {
75 let message = if text.is_empty() {
76 format!("Hot API error ({status_code})")
77 } else {
78 text.to_string()
79 };
80 ApiError {
81 status_code,
82 message,
83 code: None,
84 request_id: None,
85 retry_after: retry_after(None, headers),
86 error: None,
87 }
88 }
89 }
90}
91
92fn stringified(value: Option<&Value>) -> Option<String> {
93 match value {
94 None | Some(Value::Null) => None,
95 Some(Value::String(text)) => Some(text.clone()),
96 Some(other) => Some(other.to_string()),
97 }
98}
99
100fn retry_after(value: Option<&Value>, headers: &HeaderMap) -> Option<u64> {
101 if let Some(seconds) = value.and_then(Value::as_u64) {
102 return Some(seconds);
103 }
104 headers
105 .get("retry-after")
106 .and_then(|header| header.to_str().ok())
107 .and_then(|header| header.parse().ok())
108}