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("{message}")]
26 RunWaitFailed { message: String, run: JsonObject },
27 #[error("{message}")]
29 TaskFailed { message: String, task: JsonObject },
30 #[error("timeout waiting for run result")]
32 Timeout,
33 #[error("timeout waiting for task")]
35 TaskTimeout,
36 #[error("{0}")]
38 Protocol(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct ApiError {
44 pub status_code: u16,
45 pub message: String,
46 pub code: Option<String>,
47 pub request_id: Option<String>,
48 pub retry_after: Option<u64>,
51 pub error: Option<JsonObject>,
53}
54
55impl fmt::Display for ApiError {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{} (status {})", self.message, self.status_code)
58 }
59}
60
61impl std::error::Error for ApiError {}
62
63pub(crate) fn parse_api_error(status_code: u16, text: &str, headers: &HeaderMap) -> ApiError {
64 let parsed: Value = serde_json::from_str(text).unwrap_or(Value::Null);
65 let error_object = parsed.get("error").and_then(Value::as_object);
66
67 match error_object {
68 Some(error) => {
69 let message = error
70 .get("message")
71 .and_then(Value::as_str)
72 .map(str::to_string)
73 .unwrap_or_else(|| format!("Hot API error ({status_code})"));
74 ApiError {
75 status_code,
76 message,
77 code: stringified(error.get("code")),
78 request_id: stringified(error.get("request_id")),
79 retry_after: retry_after(error.get("retry_after"), headers),
80 error: Some(error.clone()),
81 }
82 }
83 None => {
84 let message = if text.is_empty() {
85 format!("Hot API error ({status_code})")
86 } else {
87 text.to_string()
88 };
89 ApiError {
90 status_code,
91 message,
92 code: None,
93 request_id: None,
94 retry_after: retry_after(None, headers),
95 error: None,
96 }
97 }
98 }
99}
100
101fn stringified(value: Option<&Value>) -> Option<String> {
102 match value {
103 None | Some(Value::Null) => None,
104 Some(Value::String(text)) => Some(text.clone()),
105 Some(other) => Some(other.to_string()),
106 }
107}
108
109fn retry_after(value: Option<&Value>, headers: &HeaderMap) -> Option<u64> {
110 if let Some(seconds) = value.and_then(Value::as_u64) {
111 return Some(seconds);
112 }
113 headers
114 .get("retry-after")
115 .and_then(|header| header.to_str().ok())
116 .and_then(|header| header.parse().ok())
117}