mqtt5_protocol/
error.rs

1use crate::protocol::v5::reason_codes::ReasonCode;
2use thiserror::Error;
3
4pub type Result<T> = std::result::Result<T, MqttError>;
5
6/// MQTT protocol errors
7///
8/// This enum provides specific error variants for all possible MQTT protocol
9/// errors, validation failures, and operational issues.
10///
11/// # Error Categories
12///
13/// - **I/O and Network**: `Io`, `ConnectionError`, `Timeout`, `NotConnected`
14/// - **Validation**: `InvalidTopicName`, `InvalidTopicFilter`, `InvalidClientId`
15/// - **Protocol**: `ProtocolError`, `MalformedPacket`, `InvalidPacketType`
16/// - **Authentication**: `AuthenticationFailed`, `NotAuthorized`, `BadUsernameOrPassword`
17/// - **Operations**: `SubscriptionFailed`, `PublishFailed`, `UnsubscriptionFailed`
18/// - **Server Status**: `ServerUnavailable`, `ServerBusy`, `ServerShuttingDown`
19/// - **Flow Control**: `ReceiveMaximumExceeded`, `FlowControlExceeded`, `PacketIdExhausted`
20///
21/// # Examples
22///
23/// ```
24/// use mqtt5_protocol::{MqttError, Result};
25///
26/// fn validate_topic(topic: &str) -> Result<()> {
27///     if topic.contains('#') && !topic.ends_with('#') {
28///         return Err(MqttError::InvalidTopicName(
29///             "# wildcard must be at the end".to_string()
30///         ));
31///     }
32///     Ok(())
33/// }
34/// ```
35#[derive(Error, Debug, Clone)]
36pub enum MqttError {
37    #[error("IO error: {0}")]
38    Io(String),
39
40    #[error("Invalid topic name: {0}")]
41    InvalidTopicName(String),
42
43    #[error("Invalid topic filter: {0}")]
44    InvalidTopicFilter(String),
45
46    #[error("Invalid client ID: {0}")]
47    InvalidClientId(String),
48
49    #[error("Connection error: {0}")]
50    ConnectionError(String),
51
52    #[error("Connection refused: {0:?}")]
53    ConnectionRefused(ReasonCode),
54
55    #[error("Protocol error: {0}")]
56    ProtocolError(String),
57
58    #[error("Malformed packet: {0}")]
59    MalformedPacket(String),
60
61    #[error("Packet too large: size {size} exceeds maximum {max}")]
62    PacketTooLarge { size: usize, max: usize },
63
64    #[error("Authentication failed")]
65    AuthenticationFailed,
66
67    #[error("Not authorized")]
68    NotAuthorized,
69
70    #[error("Not connected")]
71    NotConnected,
72
73    #[error("Already connected")]
74    AlreadyConnected,
75
76    #[error("Timeout")]
77    Timeout,
78
79    #[error("Subscription failed: {0:?}")]
80    SubscriptionFailed(ReasonCode),
81
82    #[error("Unsubscription failed: {0:?}")]
83    UnsubscriptionFailed(ReasonCode),
84
85    #[error("Publish failed: {0:?}")]
86    PublishFailed(ReasonCode),
87
88    #[error("Packet identifier not found: {0}")]
89    PacketIdNotFound(u16),
90
91    #[error("Packet identifier already in use: {0}")]
92    PacketIdInUse(u16),
93
94    #[error("Invalid QoS: {0}")]
95    InvalidQoS(u8),
96
97    #[error("Invalid packet type: {0}")]
98    InvalidPacketType(u8),
99
100    #[error("Invalid reason code: {0}")]
101    InvalidReasonCode(u8),
102
103    #[error("Invalid property ID: {0}")]
104    InvalidPropertyId(u8),
105
106    #[error("Duplicate property ID: {0}")]
107    DuplicatePropertyId(u8),
108
109    #[error("Session expired")]
110    SessionExpired,
111
112    #[error("Keep alive timeout")]
113    KeepAliveTimeout,
114
115    #[error("Server shutting down")]
116    ServerShuttingDown,
117
118    #[error("Client closed connection")]
119    ClientClosed,
120
121    #[error("Connection closed by peer")]
122    ConnectionClosedByPeer,
123
124    #[error("Maximum connect time exceeded")]
125    MaxConnectTime,
126
127    #[error("Topic alias invalid: {0}")]
128    TopicAliasInvalid(u16),
129
130    #[error("Receive maximum exceeded")]
131    ReceiveMaximumExceeded,
132
133    #[error("Will message rejected")]
134    WillRejected,
135
136    #[error("Implementation specific error: {0}")]
137    ImplementationSpecific(String),
138
139    #[error("Unsupported protocol version")]
140    UnsupportedProtocolVersion,
141
142    #[error("Invalid state: {0}")]
143    InvalidState(String),
144
145    #[error("Client identifier not valid")]
146    ClientIdentifierNotValid,
147
148    #[error("Bad username or password")]
149    BadUsernameOrPassword,
150
151    #[error("Server unavailable")]
152    ServerUnavailable,
153
154    #[error("Server busy")]
155    ServerBusy,
156
157    #[error("Banned")]
158    Banned,
159
160    #[error("Bad authentication method")]
161    BadAuthenticationMethod,
162
163    #[error("Quota exceeded")]
164    QuotaExceeded,
165
166    #[error("Payload format invalid")]
167    PayloadFormatInvalid,
168
169    #[error("Retain not supported")]
170    RetainNotSupported,
171
172    #[error("QoS not supported")]
173    QoSNotSupported,
174
175    #[error("Use another server")]
176    UseAnotherServer,
177
178    #[error("Server moved")]
179    ServerMoved,
180
181    #[error("Shared subscriptions not supported")]
182    SharedSubscriptionsNotSupported,
183
184    #[error("Connection rate exceeded")]
185    ConnectionRateExceeded,
186
187    #[error("Subscription identifiers not supported")]
188    SubscriptionIdentifiersNotSupported,
189
190    #[error("Wildcard subscriptions not supported")]
191    WildcardSubscriptionsNotSupported,
192
193    #[error("Message too large for queue")]
194    MessageTooLarge,
195
196    #[error("Flow control exceeded")]
197    FlowControlExceeded,
198
199    #[error("Packet ID exhausted")]
200    PacketIdExhausted,
201
202    #[error("String too long: {0} bytes exceeds maximum of 65535")]
203    StringTooLong(usize),
204
205    #[error("Configuration error: {0}")]
206    Configuration(String),
207}
208
209impl MqttError {
210    pub fn is_normal_disconnect(&self) -> bool {
211        match self {
212            Self::ClientClosed | Self::ConnectionClosedByPeer => true,
213            Self::Io(msg)
214                if msg.contains("stream has been shut down")
215                    || msg.contains("Connection reset") =>
216            {
217                true
218            }
219            _ => false,
220        }
221    }
222}
223
224impl From<std::io::Error> for MqttError {
225    fn from(err: std::io::Error) -> Self {
226        MqttError::Io(err.to_string())
227    }
228}
229
230// Error conversions for BeBytes compatibility
231impl From<String> for MqttError {
232    fn from(msg: String) -> Self {
233        MqttError::MalformedPacket(msg)
234    }
235}
236
237impl From<&str> for MqttError {
238    fn from(msg: &str) -> Self {
239        MqttError::MalformedPacket(msg.to_string())
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use std::io;
247
248    #[test]
249    fn test_error_display() {
250        let err = MqttError::InvalidTopicName("test/+/topic".to_string());
251        assert_eq!(err.to_string(), "Invalid topic name: test/+/topic");
252
253        let err = MqttError::PacketTooLarge {
254            size: 1000,
255            max: 500,
256        };
257        assert_eq!(
258            err.to_string(),
259            "Packet too large: size 1000 exceeds maximum 500"
260        );
261
262        let err = MqttError::ConnectionRefused(ReasonCode::BadUsernameOrPassword);
263        assert_eq!(err.to_string(), "Connection refused: BadUsernameOrPassword");
264    }
265
266    #[test]
267    fn test_error_from_io() {
268        let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "test");
269        let mqtt_err: MqttError = io_err.into();
270        match mqtt_err {
271            MqttError::Io(e) => assert!(e.contains("test")),
272            _ => panic!("Expected Io error"),
273        }
274    }
275
276    #[test]
277    fn test_result_type() {
278        #[allow(clippy::unnecessary_wraps)]
279        fn returns_result() -> Result<String> {
280            Ok("success".to_string())
281        }
282
283        fn returns_error() -> Result<String> {
284            Err(MqttError::NotConnected)
285        }
286
287        assert!(returns_result().is_ok());
288        assert!(returns_error().is_err());
289    }
290}