use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("API error: {code} - {message}")]
Api {
code: String,
message: String,
status: u16,
},
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Environment variable not found: {0}")]
EnvVar(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("URL error: {0}")]
Url(#[from] url::ParseError),
#[error("SSE error: {0}")]
Sse(String),
#[error("Graceful disconnect: reason={reason}, retry_ms={retry_ms}")]
GracefulDisconnect { reason: String, retry_ms: u64 },
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct ApiErrorResponse {
pub error: ApiErrorDetail,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct ApiErrorDetail {
pub code: String,
pub message: String,
}
impl Error {
pub(crate) fn from_api_response(status: u16, body: &str) -> Self {
if let Ok(err) = serde_json::from_str::<ApiErrorResponse>(body) {
Error::Api {
code: err.error.code,
message: err.error.message,
status,
}
} else {
let message = if is_html_response(body) {
format!("HTTP {status}")
} else {
body.to_string()
};
Error::Api {
code: "unknown".to_string(),
message,
status,
}
}
}
}
fn is_html_response(body: &str) -> bool {
let trimmed = body.trim_start();
trimmed.starts_with("<!DOCTYPE") || trimmed.starts_with("<html") || trimmed.starts_with("<HTML")
}
pub type Result<T> = std::result::Result<T, Error>;