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("Integration API error (HTTP {status}): {message}")]
56 Integration {
57 message: String,
58 code: Option<String>,
59 status: u16,
60 },
61
62 #[error("Session API error: {message}")]
65 SessionApi { message: String },
66
67 #[error("WebSocket connection failed: {0}")]
70 WebSocketConnect(String),
71
72 #[error("WebSocket closed (code {code}): {reason}")]
74 WebSocketClosed { code: u16, reason: String },
75
76 #[error("Deserialization error: {message}")]
79 Deserialization { message: String, body: String },
80
81 #[error("Unsupported operation: {0}")]
84 UnsupportedOperation(&'static str),
85}
86
87impl Error {
88 pub fn is_auth_expired(&self) -> bool {
91 matches!(self, Self::Authentication { .. } | Self::SessionExpired)
92 }
93
94 pub fn is_transient(&self) -> bool {
96 match self {
97 Self::Transport(e) => e.is_timeout() || e.is_connect(),
98 Self::Timeout { .. } | Self::RateLimited { .. } | Self::WebSocketConnect(_) => true,
99 _ => false,
100 }
101 }
102
103 pub fn is_not_found(&self) -> bool {
105 match self {
106 Self::Transport(e) => e.status() == Some(reqwest::StatusCode::NOT_FOUND),
107 Self::Integration { status: 404, .. } => true,
108 Self::SessionApi { message } => {
109 message.starts_with("HTTP 404") || message.contains("api.err.NotFound")
110 }
111 _ => false,
112 }
113 }
114
115 pub fn api_error_code(&self) -> Option<&str> {
117 match self {
118 Self::Integration { code, .. } => code.as_deref(),
119 _ => None,
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::Error;
127
128 #[test]
129 fn session_api_http_404_counts_as_not_found() {
130 let error = Error::SessionApi {
131 message: "HTTP 404 Not Found: {\"meta\":{\"rc\":\"error\",\"msg\":\"api.err.NotFound\"},\"data\":[]}".into(),
132 };
133
134 assert!(error.is_not_found());
135 }
136
137 #[test]
138 fn session_api_not_found_code_counts_as_not_found() {
139 let error = Error::SessionApi {
140 message: "api.err.NotFound".into(),
141 };
142
143 assert!(error.is_not_found());
144 }
145}