Skip to main content

ironfix_core/
error.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! Error types for the IronFix FIX protocol engine.
8//!
9//! This module provides a unified error hierarchy using `thiserror` for typed,
10//! domain-specific errors across all IronFix operations.
11
12use std::ops::Range;
13use thiserror::Error;
14
15/// Result type alias using [`FixError`] as the error type.
16pub type Result<T> = std::result::Result<T, FixError>;
17
18/// Top-level error type for all IronFix operations.
19#[derive(Debug, Error)]
20pub enum FixError {
21    /// Error during message decoding.
22    #[error("decode error: {0}")]
23    Decode(#[from] DecodeError),
24
25    /// Error during message encoding.
26    #[error("encode error: {0}")]
27    Encode(#[from] EncodeError),
28
29    /// Error in session layer operations.
30    #[error("session error: {0}")]
31    Session(#[from] SessionError),
32
33    /// Error in message store operations.
34    #[error("store error: {0}")]
35    Store(#[from] StoreError),
36
37    /// I/O error from underlying transport.
38    #[error("io error: {0}")]
39    Io(#[from] std::io::Error),
40}
41
42/// Errors that occur during FIX message decoding.
43#[derive(Debug, Error, Clone, PartialEq, Eq)]
44pub enum DecodeError {
45    /// Message buffer is incomplete, need more data.
46    #[error("incomplete message, need more data")]
47    Incomplete,
48
49    /// Invalid BeginString field (tag 8).
50    #[error("invalid begin string: expected 8=FIX.x.y")]
51    InvalidBeginString,
52
53    /// Missing BodyLength field (tag 9).
54    #[error("missing body length field (tag 9)")]
55    MissingBodyLength,
56
57    /// Invalid BodyLength value.
58    #[error("invalid body length value")]
59    InvalidBodyLength,
60
61    /// Missing MsgType field (tag 35).
62    #[error("missing msg type field (tag 35)")]
63    MissingMsgType,
64
65    /// Invalid MsgType value.
66    #[error("invalid msg type: {0}")]
67    InvalidMsgType(String),
68
69    /// Checksum mismatch between calculated and declared values.
70    #[error("checksum mismatch: calculated {calculated}, declared {declared}")]
71    ChecksumMismatch {
72        /// Calculated checksum value.
73        calculated: u8,
74        /// Declared checksum value in message.
75        declared: u8,
76    },
77
78    /// Invalid tag format (not a valid integer).
79    #[error("invalid tag format: {0}")]
80    InvalidTag(String),
81
82    /// Missing required field.
83    #[error("missing required field: tag {tag}")]
84    MissingRequiredField {
85        /// The tag number of the missing field.
86        tag: u32,
87    },
88
89    /// Invalid field value for the expected type.
90    #[error("invalid field value for tag {tag}: {reason}")]
91    InvalidFieldValue {
92        /// The tag number of the field.
93        tag: u32,
94        /// Description of why the value is invalid.
95        reason: String,
96    },
97
98    /// Repeating group count mismatch.
99    #[error("group count mismatch for tag {count_tag}: expected {expected}, found {actual}")]
100    GroupCountMismatch {
101        /// The tag containing the group count.
102        count_tag: u32,
103        /// Expected number of group entries.
104        expected: u32,
105        /// Actual number of group entries found.
106        actual: u32,
107    },
108
109    /// Invalid UTF-8 in string field.
110    #[error("invalid utf-8 in field: {0}")]
111    InvalidUtf8(#[from] std::str::Utf8Error),
112
113    /// Message exceeds maximum allowed size.
114    #[error("message too large: {size} bytes exceeds maximum {max_size}")]
115    MessageTooLarge {
116        /// Actual message size in bytes.
117        size: usize,
118        /// Maximum allowed size in bytes.
119        max_size: usize,
120    },
121}
122
123/// Errors that occur during FIX message encoding.
124#[derive(Debug, Error, Clone, PartialEq, Eq)]
125pub enum EncodeError {
126    /// Buffer capacity exceeded during encoding.
127    #[error("buffer overflow: need {needed} bytes, have {available}")]
128    BufferOverflow {
129        /// Bytes needed to complete encoding.
130        needed: usize,
131        /// Bytes available in buffer.
132        available: usize,
133    },
134
135    /// Missing required field during encoding.
136    #[error("missing required field: tag {tag}")]
137    MissingRequiredField {
138        /// The tag number of the missing field.
139        tag: u32,
140    },
141
142    /// Invalid field value for encoding.
143    #[error("invalid field value for tag {tag}: {reason}")]
144    InvalidFieldValue {
145        /// The tag number of the field.
146        tag: u32,
147        /// Description of why the value is invalid.
148        reason: String,
149    },
150
151    /// Field value exceeds maximum length.
152    #[error("field value too long for tag {tag}: {length} exceeds max {max_length}")]
153    FieldTooLong {
154        /// The tag number of the field.
155        tag: u32,
156        /// Actual length of the value.
157        length: usize,
158        /// Maximum allowed length.
159        max_length: usize,
160    },
161}
162
163/// Errors in FIX session layer operations.
164#[derive(Debug, Error, Clone, PartialEq, Eq)]
165pub enum SessionError {
166    /// Session is not in the correct state for the operation.
167    #[error("invalid session state: expected {expected}, current {current}")]
168    InvalidState {
169        /// Expected state for the operation.
170        expected: String,
171        /// Current session state.
172        current: String,
173    },
174
175    /// Logon was rejected by counterparty.
176    #[error("logon rejected: {reason}")]
177    LogonRejected {
178        /// Reason for rejection.
179        reason: String,
180    },
181
182    /// Heartbeat timeout - no response to TestRequest.
183    #[error("heartbeat timeout after {elapsed_ms} milliseconds")]
184    HeartbeatTimeout {
185        /// Elapsed time in milliseconds since last message.
186        elapsed_ms: u64,
187    },
188
189    /// Sequence number gap detected.
190    #[error("sequence gap detected: expected {expected}, received {received}")]
191    SequenceGap {
192        /// Expected sequence number.
193        expected: u64,
194        /// Received sequence number.
195        received: u64,
196    },
197
198    /// Sequence number too low (possible duplicate).
199    #[error("sequence too low: expected >= {expected}, received {received}")]
200    SequenceTooLow {
201        /// Minimum expected sequence number.
202        expected: u64,
203        /// Received sequence number.
204        received: u64,
205    },
206
207    /// Message rejected by counterparty.
208    #[error("message rejected: ref_seq={ref_seq_num}, reason={reason}")]
209    MessageRejected {
210        /// Reference sequence number of rejected message.
211        ref_seq_num: u64,
212        /// Rejection reason.
213        reason: String,
214    },
215
216    /// Resend request for unavailable messages.
217    #[error("resend request for unavailable range: {begin}..{end}")]
218    ResendUnavailable {
219        /// Begin sequence number of requested range.
220        begin: u64,
221        /// End sequence number of requested range.
222        end: u64,
223    },
224
225    /// Session configuration error.
226    #[error("configuration error: {0}")]
227    Configuration(String),
228
229    /// Connection error.
230    #[error("connection error: {0}")]
231    Connection(String),
232}
233
234/// Errors in message store operations.
235#[derive(Debug, Error, Clone, PartialEq, Eq)]
236pub enum StoreError {
237    /// Failed to store message.
238    #[error("failed to store message seq={seq_num}: {reason}")]
239    StoreFailed {
240        /// Sequence number of the message.
241        seq_num: u64,
242        /// Reason for failure.
243        reason: String,
244    },
245
246    /// Failed to retrieve message.
247    #[error("failed to retrieve message seq={seq_num}: {reason}")]
248    RetrieveFailed {
249        /// Sequence number of the message.
250        seq_num: u64,
251        /// Reason for failure.
252        reason: String,
253    },
254
255    /// Message not found in store.
256    #[error("message not found: seq={seq_num}")]
257    NotFound {
258        /// Sequence number of the missing message.
259        seq_num: u64,
260    },
261
262    /// Range of messages not available.
263    #[error("messages not available for range: {range:?}")]
264    RangeNotAvailable {
265        /// The requested range of sequence numbers.
266        range: Range<u64>,
267    },
268
269    /// Store is corrupted.
270    #[error("store corrupted: {reason}")]
271    Corrupted {
272        /// Description of the corruption.
273        reason: String,
274    },
275
276    /// I/O error in persistent store.
277    #[error("store i/o error: {0}")]
278    Io(String),
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_decode_error_display() {
287        let err = DecodeError::ChecksumMismatch {
288            calculated: 100,
289            declared: 200,
290        };
291        assert_eq!(
292            err.to_string(),
293            "checksum mismatch: calculated 100, declared 200"
294        );
295    }
296
297    #[test]
298    fn test_fix_error_from_decode() {
299        let decode_err = DecodeError::Incomplete;
300        let fix_err: FixError = decode_err.into();
301        assert!(matches!(fix_err, FixError::Decode(DecodeError::Incomplete)));
302    }
303
304    #[test]
305    fn test_session_error_display() {
306        let err = SessionError::SequenceGap {
307            expected: 5,
308            received: 10,
309        };
310        assert_eq!(
311            err.to_string(),
312            "sequence gap detected: expected 5, received 10"
313        );
314    }
315
316    #[test]
317    fn test_store_error_display() {
318        let err = StoreError::NotFound { seq_num: 42 };
319        assert_eq!(err.to_string(), "message not found: seq=42");
320    }
321}