1use std::fmt;
4
5#[derive(Debug, thiserror::Error)]
11pub enum BoxxkiteError {
12 #[error("boxxkite API error {status}: {code} - {message}")]
17 Api {
18 status: u16,
19 code: String,
20 message: String,
21 retryable: bool,
22 remediation: Option<String>,
23 },
24
25 #[error("connection error: {0}")]
29 Connection(#[source] reqwest::Error),
30
31 #[error("failed to decode response body: {0}")]
34 Decode(#[source] serde_json::Error),
35
36 #[error("websocket error: {0}")]
39 WebSocket(#[source] tokio_tungstenite::tungstenite::Error),
40
41 #[error("event stream error: {0}")]
46 EventStream(#[source] Box<reqwest_eventsource::Error>),
47
48 #[error("invalid configuration: {0}")]
53 Config(String),
54}
55
56impl From<reqwest::Error> for BoxxkiteError {
57 fn from(err: reqwest::Error) -> Self {
58 BoxxkiteError::Connection(err)
59 }
60}
61
62impl From<serde_json::Error> for BoxxkiteError {
63 fn from(err: serde_json::Error) -> Self {
64 BoxxkiteError::Decode(err)
65 }
66}
67
68impl From<tokio_tungstenite::tungstenite::Error> for BoxxkiteError {
69 fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
70 BoxxkiteError::WebSocket(err)
71 }
72}
73
74impl From<reqwest_eventsource::Error> for BoxxkiteError {
75 fn from(err: reqwest_eventsource::Error) -> Self {
76 BoxxkiteError::EventStream(Box::new(err))
77 }
78}
79
80impl BoxxkiteError {
81 pub fn code(&self) -> Option<&str> {
85 match self {
86 BoxxkiteError::Api { code, .. } => Some(code),
87 _ => None,
88 }
89 }
90
91 pub fn status(&self) -> Option<u16> {
93 match self {
94 BoxxkiteError::Api { status, .. } => Some(*status),
95 _ => None,
96 }
97 }
98
99 pub fn retryable(&self) -> bool {
100 match self { BoxxkiteError::Api { retryable, .. } => *retryable, _ => false }
101 }
102
103 pub fn remediation(&self) -> Option<&str> {
104 match self { BoxxkiteError::Api { remediation, .. } => remediation.as_deref(), _ => None }
105 }
106}
107
108#[derive(Debug, serde::Deserialize)]
110pub(crate) struct ErrorEnvelope {
111 pub error: ErrorBody,
112}
113
114#[derive(Debug, serde::Deserialize)]
115pub(crate) struct ErrorBody {
116 #[serde(default = "default_error_code")]
117 pub code: String,
118 #[serde(default)]
119 pub message: Option<String>,
120 #[serde(default)]
121 pub retryable: bool,
122 #[serde(default)]
123 pub remediation: Option<String>,
124}
125
126fn default_error_code() -> String {
127 "error".to_string()
128}
129
130pub(crate) fn api_error_from_bytes(status: u16, bytes: &[u8]) -> BoxxkiteError {
136 let parsed = serde_json::from_slice::<ErrorEnvelope>(bytes).ok();
137 let (code, message, retryable, remediation) = parsed
138 .map(|env| (env.error.code, env.error.message.unwrap_or_default(), env.error.retryable, env.error.remediation))
139 .unwrap_or_else(|| ("error".to_string(), format!("HTTP {status}"), status >= 500, None));
140 BoxxkiteError::Api {
141 status,
142 code,
143 message,
144 retryable,
145 remediation,
146 }
147}
148
149impl fmt::Display for ErrorBody {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 write!(
152 f,
153 "{}: {}",
154 self.code,
155 self.message.as_deref().unwrap_or("")
156 )
157 }
158}