Skip to main content

ig_client/
error.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 12/5/25
5******************************************************************************/
6use reqwest::StatusCode;
7use std::io;
8
9/// Error type for fetch operations.
10///
11/// # Deprecated
12/// This enum is dead: no library code constructs or returns it. Every fetch,
13/// network, database, and parse failure surfaces through [`AppError`] instead
14/// ([`AppError::Network`], [`AppError::Db`], [`AppError::Deserialization`]).
15/// It is kept only for backward compatibility and will be removed in a future
16/// release — migrate to [`AppError`].
17#[deprecated(
18    since = "0.12.0",
19    note = "unused dead enum; use AppError (Network / Db / Deserialization) instead"
20)]
21#[derive(Debug, thiserror::Error)]
22pub enum FetchError {
23    /// Network error from reqwest
24    #[error("network error: {0}")]
25    Reqwest(#[from] reqwest::Error),
26    /// Database error from sqlx
27    #[error("db error: {0}")]
28    Sqlx(#[from] sqlx::Error),
29    /// Error during parsing
30    #[error("parser error: {0}")]
31    Parser(String),
32}
33
34/// Error type for authentication operations
35#[derive(Debug, thiserror::Error)]
36pub enum AuthError {
37    /// Network error from reqwest
38    #[error("network error: {0}")]
39    Network(#[from] reqwest::Error),
40    /// I/O error
41    #[error("io error: {0}")]
42    Io(#[from] io::Error),
43    /// JSON serialization or deserialization error
44    #[error("json error: {0}")]
45    Json(#[from] serde_json::Error),
46    /// Other unspecified error
47    #[error("other error: {0}")]
48    Other(String),
49    /// Invalid credentials error
50    #[error("bad credentials")]
51    BadCredentials,
52    /// Unexpected HTTP status code
53    #[error("unexpected http status: {0}")]
54    Unexpected(StatusCode),
55    /// Rate limit exceeded error
56    #[error("rate limit exceeded")]
57    RateLimitExceeded,
58    /// A login / account-switch response was accepted by IG but omitted a
59    /// required session-token header (CST or X-SECURITY-TOKEN), so no usable
60    /// session could be derived. The payload holds the lowercase header name.
61    ///
62    /// This is a rejected / malformed authentication *response*, not bad caller
63    /// input — it must not be reported as [`AppError::InvalidInput`].
64    #[error("missing {0} header in login response")]
65    MissingSessionToken(String),
66}
67
68impl From<Box<dyn std::error::Error + Send + Sync>> for AuthError {
69    #[cold]
70    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
71        match e.downcast::<reqwest::Error>() {
72            Ok(req) => AuthError::Network(*req),
73            Err(e) => match e.downcast::<serde_json::Error>() {
74                Ok(js) => AuthError::Json(*js),
75                Err(e) => match e.downcast::<std::io::Error>() {
76                    Ok(ioe) => AuthError::Io(*ioe),
77                    Err(other) => AuthError::Other(other.to_string()),
78                },
79            },
80        }
81    }
82}
83
84impl From<Box<dyn std::error::Error>> for AuthError {
85    #[cold]
86    fn from(e: Box<dyn std::error::Error>) -> Self {
87        match e.downcast::<reqwest::Error>() {
88            Ok(req) => AuthError::Network(*req),
89            Err(e) => match e.downcast::<serde_json::Error>() {
90                Ok(js) => AuthError::Json(*js),
91                Err(e) => match e.downcast::<io::Error>() {
92                    Ok(ioe) => AuthError::Io(*ioe),
93                    Err(other) => AuthError::Other(other.to_string()),
94                },
95            },
96        }
97    }
98}
99
100impl From<AppError> for AuthError {
101    #[cold]
102    fn from(e: AppError) -> Self {
103        match e {
104            AppError::Network(e) => AuthError::Network(e),
105            AppError::Io(e) => AuthError::Io(e),
106            AppError::Json(e) => AuthError::Json(e),
107            AppError::Unexpected(s) => AuthError::Unexpected(s),
108            // Unwrap an already-typed auth error rather than re-stringifying it.
109            AppError::Auth(a) => a,
110            _ => AuthError::Other(e.to_string()),
111        }
112    }
113}
114
115/// General application error type
116#[derive(Debug, thiserror::Error)]
117pub enum AppError {
118    /// Network error from reqwest
119    #[error("network error: {0}")]
120    Network(#[from] reqwest::Error),
121    /// I/O error
122    #[error("io error: {0}")]
123    Io(#[from] io::Error),
124    /// JSON serialization or deserialization error
125    #[error("json error: {0}")]
126    Json(#[from] serde_json::Error),
127    /// Unexpected HTTP status code
128    #[error("unexpected http status: {0}")]
129    Unexpected(StatusCode),
130    /// Database error from sqlx
131    #[error("db error: {0}")]
132    Db(#[from] sqlx::Error),
133    /// Unauthorized access error
134    #[error("unauthorized")]
135    Unauthorized,
136    /// OAuth token expired error (requires token refresh)
137    #[error("oauth token expired")]
138    OAuthTokenExpired,
139    /// Resource not found error
140    #[error("not found")]
141    NotFound,
142    /// API rate limit exceeded
143    #[error("rate limit exceeded")]
144    RateLimitExceeded,
145    /// Historical data allowance exhausted (weekly quota of data points)
146    ///
147    /// The `allowance_expiry` field indicates the number of seconds
148    /// until the allowance resets. Retrying before that is pointless.
149    #[error("historical data allowance exceeded, resets in {allowance_expiry} seconds")]
150    HistoricalDataAllowanceExceeded {
151        /// Seconds until the weekly allowance resets
152        allowance_expiry: u64,
153    },
154    /// Error during serialization or deserialization
155    #[error("serialization error: {0}")]
156    SerializationError(String),
157    /// WebSocket communication error
158    #[error("websocket error: {0}")]
159    WebSocketError(String),
160    /// Deserialization error with details
161    #[error("deserialization error: {0}")]
162    Deserialization(String),
163    /// Invalid input error with a description of the constraint violated
164    #[error("invalid input: {0}")]
165    InvalidInput(String),
166    /// Authentication failure carrying a typed [`AuthError`].
167    ///
168    /// Login / refresh / account-switch paths surface their typed
169    /// [`AuthError`] through this variant (e.g. a login response missing a
170    /// required session-token header). Wrapping — rather than flattening —
171    /// preserves both the specific auth variant and its contextual message.
172    #[error("auth error: {0}")]
173    Auth(#[from] AuthError),
174    /// Generic error for cases that don't fit other categories
175    #[error("generic error: {0}")]
176    Generic(String),
177}
178
179impl From<Box<dyn std::error::Error>> for AppError {
180    #[cold]
181    fn from(e: Box<dyn std::error::Error>) -> Self {
182        match e.downcast::<reqwest::Error>() {
183            Ok(req) => AppError::Network(*req),
184            Err(e) => match e.downcast::<serde_json::Error>() {
185                Ok(js) => AppError::Json(*js),
186                Err(e) => match e.downcast::<std::io::Error>() {
187                    Ok(ioe) => AppError::Io(*ioe),
188                    // Preserve the original error text instead of fabricating a
189                    // fake "unexpected http status: 500" for arbitrary errors.
190                    Err(other) => AppError::Generic(other.to_string()),
191                },
192            },
193        }
194    }
195}
196
197impl From<String> for AppError {
198    #[cold]
199    fn from(e: String) -> Self {
200        AppError::Generic(e)
201    }
202}
203
204impl From<lightstreamer_rs::utils::LightstreamerError> for AppError {
205    #[cold]
206    fn from(e: lightstreamer_rs::utils::LightstreamerError) -> Self {
207        AppError::WebSocketError(e.to_string())
208    }
209}