Skip to main content

ironfix_engine/
error.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 14/7/26
5******************************************************************************/
6
7//! Engine error types.
8
9use ironfix_core::error::{DecodeError, EncodeError, StoreError};
10use ironfix_session::config::SessionConfigError;
11use ironfix_session::sequence::{SequenceCounter, SequenceExhausted};
12use ironfix_transport::CodecError;
13use std::time::Duration;
14
15/// Errors produced by the engine transport layer.
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum EngineError {
19    /// Underlying I/O failure.
20    #[error("io error: {0}")]
21    Io(#[from] std::io::Error),
22
23    /// Framing or checksum failure at the codec layer.
24    #[error("codec error: {0}")]
25    Codec(#[from] CodecError),
26
27    /// Failure decoding a framed FIX message.
28    #[error("decode error: {0}")]
29    Decode(#[from] DecodeError),
30
31    /// A message could not be encoded into a legal frame.
32    ///
33    /// Raised when a field value has no on-the-wire form — a value carrying the
34    /// SOH delimiter, or an empty one. The encoder refuses to stamp such a
35    /// frame rather than emit one whose `BodyLength` and `CheckSum` are correct
36    /// for corrupted bytes.
37    #[error("encode error: {0}")]
38    Encode(#[from] EncodeError),
39
40    /// The session configuration is not usable.
41    ///
42    /// Checked before the socket is dialled: an out-of-range knob — a
43    /// fractional `HeartBtInt`, an identity string carrying SOH or `=`, a zero
44    /// timeout — would otherwise corrupt the session's own messages.
45    /// [`ironfix_session::SessionConfigBuilder`] reports the same errors at
46    /// configuration time.
47    #[error("invalid session configuration: {0}")]
48    Config(#[from] SessionConfigError),
49
50    /// TCP connect did not complete within the configured timeout.
51    #[error("connect timed out after {0:?}")]
52    ConnectTimeout(Duration),
53
54    /// The Logon acknowledgement did not arrive within the logon timeout.
55    #[error("logon timed out after {0:?}")]
56    LogonTimeout(Duration),
57
58    /// The counterparty rejected the Logon.
59    #[error("logon rejected: {reason}")]
60    LogonRejected {
61        /// Text supplied by the counterparty, or a generic description.
62        reason: String,
63    },
64
65    /// The `HeartBtInt` (108) on the Logon acknowledgement could not be adopted
66    /// as the session's heartbeat interval.
67    ///
68    /// Raised when the ack omits the required field, carries a non-numeric
69    /// value, or confirms an interval above
70    /// [`ironfix_session::heartbeat::MAX_HEARTBEAT_INTERVAL_SECS`] — the value
71    /// drives every liveness timer in the session and is counterparty
72    /// controlled, so an unbounded one is refused. `108=0` is legal and never
73    /// raises this — it means "do not heartbeat".
74    #[error("unsupported heartbeat interval: {detail}")]
75    HeartbeatInterval {
76        /// Why the confirmed `HeartBtInt` was refused.
77        detail: String,
78    },
79
80    /// An unexpected message type arrived while awaiting the Logon
81    /// acknowledgement.
82    #[error("unexpected message during logon: 35={msg_type}")]
83    UnexpectedMessage {
84        /// The received MsgType (tag 35) value.
85        msg_type: String,
86    },
87
88    /// A sequence number violation that is fatal for the session.
89    #[error("sequence error: {0}")]
90    Sequence(String),
91
92    /// A sequence counter reached `u64::MAX`. No further messages can be
93    /// numbered until the session performs a sequence reset.
94    #[error(transparent)]
95    SequenceExhausted(#[from] SequenceExhausted),
96
97    /// A seeded initial sequence number was zero.
98    ///
99    /// FIX numbers messages from 1; a seeded `MsgSeqNum` (34) of 0 would be
100    /// rejected by every conforming counterparty. Checked before the socket is
101    /// dialled. Set through
102    /// [`Initiator::with_initial_sequences`](crate::Initiator::with_initial_sequences).
103    #[error("initial {counter} sequence number must be at least 1, was 0")]
104    InvalidInitialSequence {
105        /// Which seeded counter was zero.
106        counter: SequenceCounter,
107    },
108
109    /// The counterparty's identity fields (49/56, and 50/57 when
110    /// configured) did not match the session configuration.
111    #[error("identity mismatch: {detail}")]
112    IdentityMismatch {
113        /// Which field mismatched, with the expected and received values.
114        detail: String,
115    },
116
117    /// The Logon acknowledgement carried a `BeginString` (8) that does not
118    /// match the configured session version.
119    ///
120    /// For a FIX 5.0 / FIXT.1.1 session the configured transport
121    /// `BeginString` is `FIXT.1.1`, so an ack tagged `FIX.5.0*` — or any
122    /// other version — is not this session's acknowledgement and aborts the
123    /// handshake.
124    #[error("begin string mismatch: expected {expected}, received {received}")]
125    BeginStringMismatch {
126        /// The configured transport `BeginString`.
127        expected: String,
128        /// The `BeginString` the counterparty sent on the Logon ack.
129        received: String,
130    },
131
132    /// The counterparty's `SendingTime` (52) failed validation: absent,
133    /// unparseable, or further from the local clock than
134    /// `SessionConfig::sending_time_tolerance` allows.
135    #[error("SendingTime problem: {detail}")]
136    SendingTime {
137        /// Which check failed, with the offending value or the measured skew.
138        detail: String,
139    },
140
141    /// The configured `BeginString` cannot be framed conformantly.
142    ///
143    /// An unknown version, or `FIXT.1.1` on its own — which names the
144    /// transport version but no application version for the required
145    /// `DefaultApplVerID` (1137).
146    #[error("unsupported FIX version {version}: {detail}")]
147    UnsupportedVersion {
148        /// The configured version string.
149        version: String,
150        /// Why it cannot be framed.
151        detail: String,
152    },
153
154    /// A message-store operation the session depends on failed during setup.
155    ///
156    /// Raised by [`Initiator::connect`](crate::Initiator::connect) when the
157    /// store cannot be reset for a `ResetSeqNumFlag` (141) Logon, or refreshed to
158    /// recover its counters, before the session starts. Continuing anyway would
159    /// either file a new stream on top of a previous one's numbers or reuse a
160    /// `MsgSeqNum` the counterparty has already seen, so the session is refused
161    /// rather than started from a counter the store could not vouch for.
162    #[error("store error: {0}")]
163    Store(#[from] StoreError),
164
165    /// A write to the counterparty did not complete within the write timeout.
166    ///
167    /// A peer that stops reading parks a socket write forever once its receive
168    /// window closes, which would take the reactor's liveness timers with it.
169    /// The write is therefore bounded and its expiry closes the session, which
170    /// is the same verdict heartbeat detection would have reached.
171    #[error("write timed out after {0:?}")]
172    WriteTimeout(Duration),
173
174    /// An administrative MsgType was offered on the application send path.
175    ///
176    /// Logon (A), Logout (5), SequenceReset (4) and the rest of the
177    /// administrative set belong to the session state machine. One emitted
178    /// through [`Connection::send`](crate::Connection::send) would bypass the
179    /// typestate and the engine's phase tracking — a Logout sent that way, for
180    /// instance, never arms the logout timeout.
181    #[error("MsgType {msg_type} is administrative and belongs to the session layer")]
182    ReservedMsgType {
183        /// The offered MsgType (tag 35) value.
184        msg_type: String,
185    },
186
187    /// An outbound body carried a tag the engine stamps itself.
188    ///
189    /// See [`crate::outbound::RESERVED_TAGS`]. The frame would carry two
190    /// occurrences of the tag, which a conforming counterparty rejects or
191    /// misparses.
192    #[error(
193        "tag {tag} is stamped by the engine's standard header or trailer and must not be set on \
194         an outbound message"
195    )]
196    ReservedTag {
197        /// The offending tag.
198        tag: u32,
199    },
200
201    /// An outbound field value has no legal wire form.
202    ///
203    /// The reason never quotes the value: an outbound Logon body carries
204    /// `Password` (554) and `NewPassword` (925).
205    #[error("invalid outbound field {tag}: {reason}")]
206    InvalidField {
207        /// The offending tag.
208        tag: u32,
209        /// Why the value cannot be framed.
210        reason: String,
211    },
212
213    /// An administrative message lost a field its MsgType cannot go out without.
214    ///
215    /// A `to_admin` callback removed a required body field — `HeartBtInt` (108)
216    /// from a Logon, `TestReqID` (112) from a TestRequest, `NewSeqNo` (36) from
217    /// a SequenceReset, and the like. Emitting the message anyway would put a
218    /// malformed administrative frame on the wire that a conforming
219    /// counterparty rejects; the session refuses it instead.
220    #[error("administrative message {msg_type} is missing required field {tag}")]
221    MissingRequiredField {
222        /// The administrative MsgType (tag 35) value.
223        msg_type: String,
224        /// The required tag that is absent.
225        tag: u32,
226    },
227
228    /// The connection is closed; no more messages can be sent.
229    #[error("connection closed")]
230    Closed,
231
232    /// The builder was asked to produce an engine it was not configured for —
233    /// for example a terminal method called with no session, or with more than
234    /// one where a single-session engine is required.
235    #[error("engine configuration error: {0}")]
236    Configuration(String),
237}