mqtt5_protocol/
error.rs

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