use std::fmt;
#[derive(Debug, thiserror::Error)]
pub enum BoxxkiteError {
#[error("boxxkite API error {status}: {code} - {message}")]
Api {
status: u16,
code: String,
message: String,
retryable: bool,
remediation: Option<String>,
},
#[error("connection error: {0}")]
Connection(#[source] reqwest::Error),
#[error("failed to decode response body: {0}")]
Decode(#[source] serde_json::Error),
#[error("websocket error: {0}")]
WebSocket(#[source] tokio_tungstenite::tungstenite::Error),
#[error("event stream error: {0}")]
EventStream(#[source] Box<reqwest_eventsource::Error>),
#[error("invalid configuration: {0}")]
Config(String),
}
impl From<reqwest::Error> for BoxxkiteError {
fn from(err: reqwest::Error) -> Self {
BoxxkiteError::Connection(err)
}
}
impl From<serde_json::Error> for BoxxkiteError {
fn from(err: serde_json::Error) -> Self {
BoxxkiteError::Decode(err)
}
}
impl From<tokio_tungstenite::tungstenite::Error> for BoxxkiteError {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
BoxxkiteError::WebSocket(err)
}
}
impl From<reqwest_eventsource::Error> for BoxxkiteError {
fn from(err: reqwest_eventsource::Error) -> Self {
BoxxkiteError::EventStream(Box::new(err))
}
}
impl BoxxkiteError {
pub fn code(&self) -> Option<&str> {
match self {
BoxxkiteError::Api { code, .. } => Some(code),
_ => None,
}
}
pub fn status(&self) -> Option<u16> {
match self {
BoxxkiteError::Api { status, .. } => Some(*status),
_ => None,
}
}
pub fn retryable(&self) -> bool {
match self { BoxxkiteError::Api { retryable, .. } => *retryable, _ => false }
}
pub fn remediation(&self) -> Option<&str> {
match self { BoxxkiteError::Api { remediation, .. } => remediation.as_deref(), _ => None }
}
}
#[derive(Debug, serde::Deserialize)]
pub(crate) struct ErrorEnvelope {
pub error: ErrorBody,
}
#[derive(Debug, serde::Deserialize)]
pub(crate) struct ErrorBody {
#[serde(default = "default_error_code")]
pub code: String,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub retryable: bool,
#[serde(default)]
pub remediation: Option<String>,
}
fn default_error_code() -> String {
"error".to_string()
}
pub(crate) fn api_error_from_bytes(status: u16, bytes: &[u8]) -> BoxxkiteError {
let parsed = serde_json::from_slice::<ErrorEnvelope>(bytes).ok();
let (code, message, retryable, remediation) = parsed
.map(|env| (env.error.code, env.error.message.unwrap_or_default(), env.error.retryable, env.error.remediation))
.unwrap_or_else(|| ("error".to_string(), format!("HTTP {status}"), status >= 500, None));
BoxxkiteError::Api {
status,
code,
message,
retryable,
remediation,
}
}
impl fmt::Display for ErrorBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {}",
self.code,
self.message.as_deref().unwrap_or("")
)
}
}