1use thiserror::Error;
2
3#[derive(Debug, Error)]
9pub enum Error {
10 #[error("Authentication failed: {message}")]
13 Authentication { message: String },
14
15 #[error("Two-factor authentication token required")]
17 TwoFactorRequired,
18
19 #[error("Session expired -- re-authentication required")]
21 SessionExpired,
22
23 #[error("Invalid API key")]
25 InvalidApiKey,
26
27 #[error("Wrong auth strategy: expected {expected}, got {got}")]
29 WrongAuthStrategy { expected: String, got: String },
30
31 #[error("HTTP transport error: {0}")]
34 Transport(#[from] reqwest::Error),
35
36 #[error("Invalid URL: {0}")]
38 InvalidUrl(#[from] url::ParseError),
39
40 #[error("Request timed out after {timeout_secs}s")]
42 Timeout { timeout_secs: u64 },
43
44 #[error("TLS error: {0}")]
46 Tls(String),
47
48 #[error("Rate limited -- retry after {retry_after_secs}s")]
51 RateLimited { retry_after_secs: u64 },
52
53 #[error("Cloud console offline or unreachable: {host_id}")]
55 ConsoleOffline { host_id: String },
56
57 #[error("Not authorized to access cloud console: {host_id}")]
59 ConsoleAccessDenied { host_id: String },
60
61 #[error("Integration API error (HTTP {status}): {message}")]
64 Integration {
65 message: String,
66 code: Option<String>,
67 status: u16,
68 },
69
70 #[error("Session API error: {message}")]
73 SessionApi { message: String },
74
75 #[error("WebSocket connection failed: {0}")]
78 WebSocketConnect(String),
79
80 #[error("WebSocket closed (code {code}): {reason}")]
82 WebSocketClosed { code: u16, reason: String },
83
84 #[error("Deserialization error: {message}")]
87 Deserialization { message: String, body: String },
88
89 #[error("Unsupported operation: {0}")]
92 UnsupportedOperation(&'static str),
93}
94
95impl Error {
96 pub fn is_auth_expired(&self) -> bool {
99 matches!(self, Self::Authentication { .. } | Self::SessionExpired)
100 }
101
102 pub fn is_transient(&self) -> bool {
104 match self {
105 Self::Transport(e) => e.is_timeout() || e.is_connect(),
106 Self::Timeout { .. }
107 | Self::RateLimited { .. }
108 | Self::ConsoleOffline { .. }
109 | Self::WebSocketConnect(_) => true,
110 _ => false,
111 }
112 }
113
114 pub fn is_not_found(&self) -> bool {
116 match self {
117 Self::Transport(e) => e.status() == Some(reqwest::StatusCode::NOT_FOUND),
118 Self::Integration { status: 404, .. } => true,
119 Self::SessionApi { message } => {
120 message.starts_with("HTTP 404") || message.contains("api.err.NotFound")
121 }
122 _ => false,
123 }
124 }
125
126 pub fn api_error_code(&self) -> Option<&str> {
128 match self {
129 Self::Integration { code, .. } => code.as_deref(),
130 _ => None,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::Error;
138
139 #[test]
140 fn session_api_http_404_counts_as_not_found() {
141 let error = Error::SessionApi {
142 message: "HTTP 404 Not Found: {\"meta\":{\"rc\":\"error\",\"msg\":\"api.err.NotFound\"},\"data\":[]}".into(),
143 };
144
145 assert!(error.is_not_found());
146 }
147
148 #[test]
149 fn session_api_not_found_code_counts_as_not_found() {
150 let error = Error::SessionApi {
151 message: "api.err.NotFound".into(),
152 };
153
154 assert!(error.is_not_found());
155 }
156}