1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum Error {
8 #[error("API error: {code} - {message}")]
10 Api {
11 code: String,
12 message: String,
13 status: u16,
14 },
15
16 #[error("Network error: {0}")]
18 Network(#[from] reqwest::Error),
19
20 #[error("Authentication error: {0}")]
22 Auth(String),
23
24 #[error("Environment variable not found: {0}")]
26 EnvVar(String),
27
28 #[error("JSON error: {0}")]
30 Json(#[from] serde_json::Error),
31
32 #[error("URL error: {0}")]
34 Url(#[from] url::ParseError),
35
36 #[error("SSE error: {0}")]
38 Sse(String),
39
40 #[error("Graceful disconnect: reason={reason}, retry_ms={retry_ms}")]
42 GracefulDisconnect { reason: String, retry_ms: u64 },
43}
44
45#[derive(Debug, serde::Serialize, serde::Deserialize)]
47#[non_exhaustive]
48pub struct ApiErrorResponse {
49 pub error: ApiErrorDetail,
50}
51
52#[derive(Debug, serde::Serialize, serde::Deserialize)]
54#[non_exhaustive]
55pub struct ApiErrorDetail {
56 pub code: String,
57 pub message: String,
58}
59
60impl Error {
61 pub(crate) fn from_api_response(status: u16, body: &str) -> Self {
62 if let Ok(err) = serde_json::from_str::<ApiErrorResponse>(body) {
63 Error::Api {
64 code: err.error.code,
65 message: err.error.message,
66 status,
67 }
68 } else {
69 let message = if is_html_response(body) {
71 format!("HTTP {status}")
72 } else {
73 body.to_string()
74 };
75 Error::Api {
76 code: "unknown".to_string(),
77 message,
78 status,
79 }
80 }
81 }
82}
83
84fn is_html_response(body: &str) -> bool {
86 let trimmed = body.trim_start();
87 trimmed.starts_with("<!DOCTYPE") || trimmed.starts_with("<html") || trimmed.starts_with("<HTML")
88}
89
90pub type Result<T> = std::result::Result<T, Error>;