Skip to main content

hot_dev/
error.rs

1use std::fmt;
2
3use reqwest::header::HeaderMap;
4use serde_json::Value;
5
6use crate::JsonObject;
7
8/// The error type for SDK operations.
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    /// The Hot API returned a non-2xx response.
12    #[error(transparent)]
13    Api(#[from] ApiError),
14    /// The HTTP request itself failed.
15    #[error("transport error: {0}")]
16    Http(#[from] reqwest::Error),
17    /// A response body was not valid JSON.
18    #[error("invalid JSON response: {0}")]
19    Json(#[from] serde_json::Error),
20    /// A run ended with `run:fail` or `run:cancel`; carries the run's result
21    /// message.
22    #[error("{0}")]
23    RunFailed(String),
24    /// A waited run reached a failed or cancelled terminal state.
25    #[error("{message}")]
26    RunWaitFailed { message: String, run: JsonObject },
27    /// A task reached a failed, cancelled, or timed-out terminal state.
28    #[error("{message}")]
29    TaskFailed { message: String, task: JsonObject },
30    /// Timed out waiting for a run result.
31    #[error("timeout waiting for run result")]
32    Timeout,
33    /// Timed out waiting for a task result.
34    #[error("timeout waiting for task")]
35    TaskTimeout,
36    /// The stream ended before the expected event arrived.
37    #[error("{0}")]
38    Protocol(String),
39}
40
41/// Structured error for non-2xx Hot API responses.
42#[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    /// Server-suggested retry delay in seconds, from the error body or
49    /// Retry-After header.
50    pub retry_after: Option<u64>,
51    /// The raw "error" object from the response body, if any.
52    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}