kiteconnect_async_wasm/models/common/
errors.rs

1/*!
2Custom error types for KiteConnect operations using `thiserror`.
3
4Provides comprehensive error handling with proper error chaining and context.
5*/
6
7use thiserror::Error;
8
9/// Main error type for all KiteConnect operations
10#[derive(Debug, Error)]
11pub enum KiteError {
12    /// HTTP request failed
13    #[error("HTTP request failed: {0}")]
14    Http(#[from] reqwest::Error),
15
16    /// JSON parsing failed
17    #[error("JSON parsing failed: {0}")]
18    Json(#[from] serde_json::Error),
19
20    // === Official KiteConnect API Exception Types ===
21    /// Session expired or invalidated (403 header)
22    /// User should clear session and re-initiate login
23    #[error("Token exception: {0}")]
24    TokenException(String),
25
26    /// User account related errors
27    #[error("User exception: {0}")]
28    UserException(String),
29
30    /// Order related errors (placement failures, corrupt fetch, etc.)
31    #[error("Order exception: {0}")]
32    OrderException(String),
33
34    /// Missing required fields, bad parameter values
35    #[error("Input exception: {0}")]
36    InputException(String),
37
38    /// Insufficient funds required for order placement
39    #[error("Margin exception: {0}")]
40    MarginException(String),
41
42    /// Insufficient holdings available to place sell order
43    #[error("Holding exception: {0}")]
44    HoldingException(String),
45
46    /// Network error - API unable to communicate with OMS
47    #[error("Network exception: {0}")]
48    NetworkException(String),
49
50    /// Internal system error - API unable to understand OMS response
51    #[error("Data exception: {0}")]
52    DataException(String),
53
54    /// Unclassified error (should be rare)
55    #[error("General exception: {0}")]
56    GeneralException(String),
57
58    // === Generic fallback errors ===
59    /// Generic API error when error_type is not recognized
60    #[error("API error: {status} - {message}")]
61    Api {
62        status: String,
63        message: String,
64        error_type: Option<String>,
65    },
66
67    /// Authentication failed (generic)
68    #[error("Authentication failed: {0}")]
69    Authentication(String),
70
71    /// Invalid parameter provided (generic)
72    #[error("Invalid parameter: {0}")]
73    InvalidParameter(String),
74
75    /// CSV parsing failed (for instruments data)
76    #[cfg(feature = "native")]
77    #[error("CSV parsing failed: {0}")]
78    CsvParsing(#[from] csv::Error),
79
80    /// Date/time parsing failed
81    #[error("Date/time parsing failed: {0}")]
82    DateTimeParsing(#[from] chrono::ParseError),
83
84    /// URL parsing failed
85    #[error("URL parsing failed: {0}")]
86    UrlParsing(#[from] url::ParseError),
87
88    /// General error with custom message
89    #[error("KiteConnect error: {0}")]
90    General(String),
91
92    /// Backward compatibility with anyhow errors
93    #[error("Legacy error: {0}")]
94    Legacy(#[from] anyhow::Error),
95}
96
97/// Result type alias for KiteConnect operations
98pub type KiteResult<T> = Result<T, KiteError>;
99
100impl KiteError {
101    /// Create a new API error from response
102    /// Maps official KiteConnect error_type to specific exception types
103    pub fn from_api_response(
104        status_code: u16,
105        status: impl Into<String>,
106        message: impl Into<String>,
107        error_type: Option<String>,
108    ) -> Self {
109        let message = message.into();
110
111        // First, map based on error_type from API response
112        if let Some(error_type) = error_type.as_ref() {
113            return match error_type.as_str() {
114                "TokenException" => Self::TokenException(message),
115                "UserException" => Self::UserException(message),
116                "OrderException" => Self::OrderException(message),
117                "InputException" => Self::InputException(message),
118                "MarginException" => Self::MarginException(message),
119                "HoldingException" => Self::HoldingException(message),
120                "NetworkException" => Self::NetworkException(message),
121                "DataException" => Self::DataException(message),
122                "GeneralException" => Self::GeneralException(message),
123                _ => Self::Api {
124                    status: status.into(),
125                    message,
126                    error_type: Some(error_type.clone()),
127                },
128            };
129        }
130
131        // Fallback: map based on HTTP status code
132        match status_code {
133            400 => Self::InputException(message), // Missing or bad request parameters or values
134            403 => Self::TokenException(message), // Session expired or invalidate. Must relogin
135            404 => Self::Api {
136                status: status.into(),
137                message,
138                error_type: Some("ResourceNotFound".to_string()), // Request resource was not found
139            },
140            405 => Self::Api {
141                status: status.into(),
142                message,
143                error_type: Some("MethodNotAllowed".to_string()), // Request method (GET, POST etc.) is not allowed on the requested endpoint
144            },
145            410 => Self::Api {
146                status: status.into(),
147                message,
148                error_type: Some("ResourceGone".to_string()), // The requested resource is gone permanently
149            },
150            429 => Self::Api {
151                status: status.into(),
152                message,
153                error_type: Some("RateLimited".to_string()), // Too many requests to the API (rate limiting)
154            },
155            500 => Self::GeneralException(message), // Something unexpected went wrong
156            502 => Self::NetworkException(message), // The backend OMS is down and the API is unable to communicate with it
157            503 => Self::NetworkException(message), // Service unavailable; the API is down
158            504 => Self::NetworkException(message), // Gateway timeout; the API is unreachable
159            _ => Self::Api {
160                status: status.into(),
161                message,
162                error_type,
163            },
164        }
165    }
166
167    /// Create a new API error (legacy method for backward compatibility)
168    pub fn api_error(status: impl Into<String>, message: impl Into<String>) -> Self {
169        Self::Api {
170            status: status.into(),
171            message: message.into(),
172            error_type: None,
173        }
174    }
175
176    /// Create a new API error with error type (legacy method for backward compatibility)
177    pub fn api_error_with_type(
178        status: impl Into<String>,
179        message: impl Into<String>,
180        error_type: impl Into<String>,
181    ) -> Self {
182        Self::Api {
183            status: status.into(),
184            message: message.into(),
185            error_type: Some(error_type.into()),
186        }
187    }
188
189    /// Create a new authentication error
190    pub fn auth_error(message: impl Into<String>) -> Self {
191        Self::Authentication(message.into())
192    }
193
194    /// Create a new invalid parameter error
195    pub fn invalid_param(message: impl Into<String>) -> Self {
196        Self::InvalidParameter(message.into())
197    }
198
199    /// Create a new general error
200    pub fn general(message: impl Into<String>) -> Self {
201        Self::General(message.into())
202    }
203
204    // === Official KiteConnect Exception Constructors ===
205
206    /// Create a new TokenException
207    pub fn token_exception(message: impl Into<String>) -> Self {
208        Self::TokenException(message.into())
209    }
210
211    /// Create a new UserException
212    pub fn user_exception(message: impl Into<String>) -> Self {
213        Self::UserException(message.into())
214    }
215
216    /// Create a new OrderException
217    pub fn order_exception(message: impl Into<String>) -> Self {
218        Self::OrderException(message.into())
219    }
220
221    /// Create a new InputException
222    pub fn input_exception(message: impl Into<String>) -> Self {
223        Self::InputException(message.into())
224    }
225
226    /// Create a new MarginException
227    pub fn margin_exception(message: impl Into<String>) -> Self {
228        Self::MarginException(message.into())
229    }
230
231    /// Create a new HoldingException
232    pub fn holding_exception(message: impl Into<String>) -> Self {
233        Self::HoldingException(message.into())
234    }
235
236    /// Create a new NetworkException
237    pub fn network_exception(message: impl Into<String>) -> Self {
238        Self::NetworkException(message.into())
239    }
240
241    /// Create a new DataException
242    pub fn data_exception(message: impl Into<String>) -> Self {
243        Self::DataException(message.into())
244    }
245
246    /// Create a new GeneralException
247    pub fn general_exception(message: impl Into<String>) -> Self {
248        Self::GeneralException(message.into())
249    }
250
251    /// Check if this error requires re-authentication
252    pub fn requires_reauth(&self) -> bool {
253        matches!(self, Self::TokenException(_) | Self::Authentication(_))
254    }
255
256    /// Check if this is a client-side error (4xx)
257    pub fn is_client_error(&self) -> bool {
258        match self {
259            Self::TokenException(_) | Self::InputException(_) | Self::InvalidParameter(_) => true,
260            Self::Api { status, .. } => status.starts_with('4'),
261            _ => false,
262        }
263    }
264
265    /// Check if this is a server-side error (5xx)
266    pub fn is_server_error(&self) -> bool {
267        match self {
268            Self::NetworkException(_) | Self::DataException(_) | Self::GeneralException(_) => true,
269            Self::Api { status, .. } => status.starts_with('5'),
270            _ => false,
271        }
272    }
273
274    /// Check if this error can be retried
275    pub fn is_retryable(&self) -> bool {
276        match self {
277            Self::NetworkException(_) | Self::Http(_) => true, // Includes 502, 503, 504 network errors
278            Self::Api { status, .. } => matches!(status.as_str(), "429"), // Only rate limiting is retryable for API errors
279            _ => false,
280        }
281    }
282}