Skip to main content

ibapi/
errors.rs

1//! Error types for the IBAPI library.
2//!
3//! This module defines all error types that can occur during API operations,
4//! including I/O errors, parsing errors, and TWS-specific protocol errors.
5
6use std::{num::ParseIntError, string::FromUtf8Error};
7use thiserror::Error;
8
9use crate::market_data::historical::HistoricalParseError;
10use crate::messages::{ResponseMessage, CODE_INDEX, MESSAGE_INDEX};
11use crate::orders::builder::ValidationError;
12
13/// The main error type for IBAPI operations.
14///
15/// This enum is marked `#[non_exhaustive]` to allow adding new error variants
16/// in future versions without breaking compatibility.
17#[derive(Debug, Error)]
18#[non_exhaustive]
19pub enum Error {
20    // External error types
21    /// I/O error from network operations.
22    #[error(transparent)]
23    Io(#[from] std::io::Error),
24
25    /// Failed to parse an integer from string.
26    #[error(transparent)]
27    ParseInt(#[from] ParseIntError),
28
29    /// Invalid UTF-8 sequence in response data.
30    #[error(transparent)]
31    FromUtf8(#[from] FromUtf8Error),
32
33    /// Failed to parse time/date string.
34    #[error(transparent)]
35    ParseTime(#[from] time::error::Parse),
36
37    /// Mutex was poisoned by a panic in another thread.
38    #[error("{0}")]
39    Poison(String),
40
41    // IBAPI-specific errors
42    /// Feature or method not yet implemented.
43    #[error("not implemented")]
44    NotImplemented,
45
46    /// Failed to parse a protocol message.
47    /// Contains: (field_index, field_value, error_description)
48    #[error("parse error: {0} - {1} - {2}")]
49    Parse(usize, String, String),
50
51    /// Server version requirement not met.
52    /// Contains: (required_version, actual_version, feature_name)
53    #[error("server version {0} required, got {1}: {2}")]
54    ServerVersion(i32, i32, String),
55
56    /// Generic error with custom message.
57    #[error("error occurred: {0}")]
58    Simple(String),
59
60    /// Invalid argument provided to API method.
61    #[error("InvalidArgument: {0}")]
62    InvalidArgument(String),
63
64    /// Failed to establish connection to TWS/Gateway.
65    #[error("ConnectionFailed")]
66    ConnectionFailed,
67
68    /// Connection was reset by TWS/Gateway.
69    #[error("ConnectionReset")]
70    ConnectionReset,
71
72    /// Operation was cancelled by user or system.
73    #[error("Cancelled")]
74    Cancelled,
75
76    /// Client is shutting down.
77    #[error("Shutdown")]
78    Shutdown,
79
80    /// Reached end of data stream.
81    #[error("EndOfStream")]
82    EndOfStream,
83
84    /// Received unexpected message type.
85    #[error("UnexpectedResponse: {0:?}")]
86    UnexpectedResponse(ResponseMessage),
87
88    /// Stream ended unexpectedly.
89    #[error("UnexpectedEndOfStream")]
90    UnexpectedEndOfStream,
91
92    /// Error message from TWS/Gateway.
93    /// Contains: (error_code, error_message)
94    #[error("[{0}] {1}")]
95    Message(i32, String),
96
97    /// Attempted to create a duplicate subscription.
98    #[error("AlreadySubscribed")]
99    AlreadySubscribed,
100
101    /// Wraps errors parsing historical data parameters.
102    #[error("HistoricalParseError: {0}")]
103    HistoricalParseError(HistoricalParseError),
104}
105
106impl From<ResponseMessage> for Error {
107    fn from(err: ResponseMessage) -> Error {
108        let code = err.peek_int(CODE_INDEX).unwrap();
109        let message = err.peek_string(MESSAGE_INDEX);
110        Error::Message(code, message)
111    }
112}
113
114impl<T> From<std::sync::PoisonError<T>> for Error {
115    fn from(err: std::sync::PoisonError<T>) -> Error {
116        Error::Poison(format!("Mutex poison error: {err}"))
117    }
118}
119
120impl From<ValidationError> for Error {
121    fn from(err: ValidationError) -> Self {
122        match err {
123            ValidationError::InvalidQuantity(q) => Error::InvalidArgument(format!("Invalid quantity: {}", q)),
124            ValidationError::InvalidPrice(p) => Error::InvalidArgument(format!("Invalid price: {}", p)),
125            ValidationError::MissingRequiredField(field) => Error::InvalidArgument(format!("Missing required field: {}", field)),
126            ValidationError::InvalidCombination(msg) => Error::InvalidArgument(format!("Invalid combination: {}", msg)),
127            ValidationError::InvalidStopPrice { stop, current } => {
128                Error::InvalidArgument(format!("Invalid stop price {} for current price {}", stop, current))
129            }
130            ValidationError::InvalidLimitPrice { limit, current } => {
131                Error::InvalidArgument(format!("Invalid limit price {} for current price {}", limit, current))
132            }
133            ValidationError::InvalidBracketOrder(msg) => Error::InvalidArgument(format!("Invalid bracket order: {}", msg)),
134            ValidationError::InvalidPercentage { field, value, min, max } => {
135                Error::InvalidArgument(format!("Invalid {}: {} (must be between {} and {})", field, value, min, max))
136            }
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::error::Error as StdError;
145    use std::io;
146    use std::sync::{Mutex, PoisonError};
147    use time::macros::format_description;
148    use time::Time;
149
150    #[test]
151    fn test_error_debug() {
152        let error = Error::Simple("test error".to_string());
153        assert_eq!(format!("{error:?}"), "Simple(\"test error\")");
154    }
155
156    #[test]
157    fn test_error_display() {
158        let cases = vec![
159            (Error::Io(io::Error::new(io::ErrorKind::NotFound, "file not found")), "file not found"),
160            (Error::ParseInt("123x".parse::<i32>().unwrap_err()), "invalid digit found in string"),
161            (
162                Error::FromUtf8(String::from_utf8(vec![0, 159, 146, 150]).unwrap_err()),
163                "invalid utf-8 sequence of 1 bytes from index 1",
164            ),
165            (
166                Error::ParseTime(Time::parse("2021-13-01", format_description!("[year]-[month]-[day]")).unwrap_err()),
167                "the 'month' component could not be parsed",
168            ),
169            (Error::Poison("test poison".to_string()), "test poison"),
170            (Error::NotImplemented, "not implemented"),
171            (
172                Error::Parse(1, "value".to_string(), "message".to_string()),
173                "parse error: 1 - value - message",
174            ),
175            (
176                Error::ServerVersion(2, 1, "old version".to_string()),
177                "server version 2 required, got 1: old version",
178            ),
179            (Error::ConnectionFailed, "ConnectionFailed"),
180            (Error::Cancelled, "Cancelled"),
181            (Error::Simple("simple error".to_string()), "error occurred: simple error"),
182        ];
183
184        for (error, expected) in cases {
185            assert_eq!(error.to_string(), expected);
186        }
187    }
188
189    #[test]
190    fn test_error_is_error() {
191        let error = Error::Simple("test error".to_string());
192        // With thiserror, source() returns the underlying error if using #[from]
193        // For Simple errors, there's no underlying source
194        assert!(error.source().is_none());
195    }
196
197    #[test]
198    fn test_from_io_error() {
199        let io_error = io::Error::other("io error");
200        let error: Error = io_error.into();
201        assert!(matches!(error, Error::Io(_)));
202    }
203
204    #[test]
205    fn test_from_parse_int_error() {
206        let parse_error = "abc".parse::<i32>().unwrap_err();
207        let error: Error = parse_error.into();
208        assert!(matches!(error, Error::ParseInt(_)));
209    }
210
211    #[test]
212    fn test_from_utf8_error() {
213        let utf8_error = String::from_utf8(vec![0, 159, 146, 150]).unwrap_err();
214        let error: Error = utf8_error.into();
215        assert!(matches!(error, Error::FromUtf8(_)));
216    }
217
218    #[test]
219    fn test_from_parse_time_error() {
220        let time_error = Time::parse("2021-13-01", format_description!("[year]-[month]-[day]")).unwrap_err();
221        let error: Error = time_error.into();
222        assert!(matches!(error, Error::ParseTime(_)));
223    }
224
225    #[test]
226    fn test_from_poison_error() {
227        let mutex = Mutex::new(());
228        let poison_error = PoisonError::new(mutex);
229        let error: Error = poison_error.into();
230        assert!(matches!(error, Error::Poison(_)));
231    }
232
233    #[test]
234    fn test_non_exhaustive() {
235        fn assert_non_exhaustive<T: StdError>() {}
236        assert_non_exhaustive::<Error>();
237    }
238}