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