Skip to main content

trading_ig/
error.rs

1//! Crate-wide error type.
2//!
3//! Domain modules **must not** define their own error enum — extend [`Error`]
4//! instead. Variants are intentionally coarse-grained; the `Api` variant
5//! carries IG's machine-readable `errorCode` for fine matching by callers.
6
7use http::StatusCode;
8use serde::Deserialize;
9
10/// Convenience alias used throughout the crate.
11pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    #[error("HTTP transport error: {0}")]
16    Http(#[from] reqwest::Error),
17
18    #[error("IG API error ({status}): {0}", .source.error_code)]
19    Api {
20        status: StatusCode,
21        #[source]
22        source: ApiError,
23    },
24
25    #[error("authentication error: {0}")]
26    Auth(String),
27
28    #[error("rate limited by IG ({0})")]
29    RateLimited(String),
30
31    #[error("failed to deserialise response: {0}")]
32    Deserialization(#[from] serde_json::Error),
33
34    #[error("invalid configuration: {0}")]
35    Config(String),
36
37    #[error("invalid input: {0}")]
38    InvalidInput(String),
39
40    #[error("URL error: {0}")]
41    Url(#[from] url::ParseError),
42
43    #[error("invalid HTTP header value: {0}")]
44    HeaderValue(#[from] http::header::InvalidHeaderValue),
45
46    /// LS control rejection (`200 OK` body `ERROR ...`). Not auth: must not
47    /// trigger a token refresh.
48    #[error("streaming error: {0}")]
49    Streaming(String),
50}
51
52/// Wire-level error payload returned by IG.
53///
54/// Most endpoints return `{ "errorCode": "…" }` on failure. Some include
55/// additional context fields, surfaced via the `extra` map.
56#[derive(Debug, Clone, Deserialize, thiserror::Error)]
57#[error("{error_code}")]
58pub struct ApiError {
59    #[serde(rename = "errorCode")]
60    pub error_code: String,
61    #[serde(flatten, default)]
62    pub extra: serde_json::Map<String, serde_json::Value>,
63}
64
65impl Error {
66    /// True for `Error::Auth` or for `Error::Api` whose IG `errorCode`
67    /// matches one of: `oauth-token-invalid`, `client-token-invalid`,
68    /// `client-token-missing`, `security-token-*`.
69    pub fn is_auth(&self) -> bool {
70        match self {
71            Error::Auth(_) => true,
72            Error::Api { source, .. } => {
73                let c = source.error_code.as_str();
74                c.contains("oauth-token-invalid")
75                    || c.contains("client-token-invalid")
76                    || c.contains("client-token-missing")
77                    || c.contains("security-token")
78            }
79            _ => false,
80        }
81    }
82
83    /// True if the error indicates the rate limit has been hit.
84    pub fn is_rate_limited(&self) -> bool {
85        match self {
86            Error::RateLimited(_) => true,
87            Error::Api { source, .. } => source.error_code.contains("api-rate-exceeded"),
88            _ => false,
89        }
90    }
91}