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