titanium_gateway/
error.rs

1//! Gateway error types using thiserror.
2//!
3//! All errors in titanium-gateway are represented by the [`GatewayError`] enum.
4//! No `.unwrap()` calls are used throughout the codebase.
5
6use thiserror::Error;
7
8/// Errors that can occur during Gateway operations.
9#[derive(Debug, Error)]
10pub enum GatewayError {
11    /// WebSocket connection or protocol error.
12    #[error("WebSocket error: {0}")]
13    WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
14
15    /// Failed to parse JSON payload.
16    #[error("JSON decode error: {0}")]
17    JsonDecode(String),
18
19    /// Session was invalidated by Discord.
20    /// The boolean indicates if the session is resumable.
21    #[error("Session invalidated, resumable: {resumable}")]
22    InvalidSession {
23        /// Whether the session can be resumed.
24        resumable: bool,
25    },
26
27    /// Connection was closed by Discord.
28    #[error("Connection closed: code={code}, reason={reason}")]
29    Closed {
30        /// WebSocket close code.
31        code: u16,
32        /// Close reason.
33        reason: String,
34    },
35
36    /// Heartbeat acknowledgment was not received in time.
37    #[error("Heartbeat acknowledgment timeout")]
38    HeartbeatTimeout,
39
40    /// Authentication failed (invalid token, missing intents, etc.).
41    #[error("Authentication failed: {0}")]
42    AuthenticationFailed(String),
43
44    /// Unknown or unhandled event type.
45    #[error("Unknown event type: {0}")]
46    UnknownEvent(String),
47
48    /// Failed to send message through channel.
49    #[error("Channel send error: {0}")]
50    ChannelSend(String),
51
52    /// URL parsing error.
53    #[error("URL parse error: {0}")]
54    UrlParse(#[from] url::ParseError),
55
56    /// I/O error.
57    #[error("I/O error: {0}")]
58    Io(#[from] std::io::Error),
59
60    /// Shard is not connected.
61    #[error("Shard not connected")]
62    NotConnected,
63
64    /// Rate limited.
65    #[error("Rate limited, retry after {retry_after_ms}ms")]
66    RateLimited {
67        /// Milliseconds until rate limit expires.
68        retry_after_ms: u64,
69    },
70}
71
72impl From<serde_json::Error> for GatewayError {
73    fn from(err: serde_json::Error) -> Self {
74        GatewayError::JsonDecode(err.to_string())
75    }
76}
77
78#[cfg(feature = "simd")]
79impl From<simd_json::Error> for GatewayError {
80    fn from(err: simd_json::Error) -> Self {
81        GatewayError::JsonDecode(err.to_string())
82    }
83}
84
85impl<T> From<flume::SendError<T>> for GatewayError {
86    fn from(err: flume::SendError<T>) -> Self {
87        GatewayError::ChannelSend(err.to_string())
88    }
89}
90
91/// Discord Gateway close codes.
92///
93/// See: <https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes>
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95#[repr(u16)]
96pub enum CloseCode {
97    /// Unknown error occurred.
98    UnknownError = 4000,
99    /// Invalid opcode sent.
100    UnknownOpcode = 4001,
101    /// Invalid payload (decode error).
102    DecodeError = 4002,
103    /// Payload sent before identifying.
104    NotAuthenticated = 4003,
105    /// Invalid token.
106    AuthenticationFailed = 4004,
107    /// Already authenticated.
108    AlreadyAuthenticated = 4005,
109    /// Invalid sequence number for resume.
110    InvalidSeq = 4007,
111    /// Rate limited.
112    RateLimited = 4008,
113    /// Session timed out.
114    SessionTimedOut = 4009,
115    /// Invalid shard configuration.
116    InvalidShard = 4010,
117    /// Too many guilds (sharding required).
118    ShardingRequired = 4011,
119    /// Invalid API version.
120    InvalidApiVersion = 4012,
121    /// Invalid intents.
122    InvalidIntents = 4013,
123    /// Disallowed intents (privileged intent not enabled).
124    DisallowedIntents = 4014,
125}
126
127impl CloseCode {
128    /// Returns whether this close code is recoverable by resuming.
129    #[allow(dead_code)]
130    pub const fn is_resumable(self) -> bool {
131        matches!(
132            self,
133            CloseCode::UnknownError | CloseCode::InvalidSeq | CloseCode::SessionTimedOut
134        )
135    }
136
137    /// Returns whether reconnection is possible after this close code.
138    pub const fn can_reconnect(self) -> bool {
139        !matches!(
140            self,
141            CloseCode::AuthenticationFailed
142                | CloseCode::InvalidShard
143                | CloseCode::ShardingRequired
144                | CloseCode::InvalidApiVersion
145                | CloseCode::InvalidIntents
146                | CloseCode::DisallowedIntents
147        )
148    }
149
150    /// Try to convert a u16 close code to this enum.
151    pub fn from_code(code: u16) -> Option<Self> {
152        match code {
153            4000 => Some(CloseCode::UnknownError),
154            4001 => Some(CloseCode::UnknownOpcode),
155            4002 => Some(CloseCode::DecodeError),
156            4003 => Some(CloseCode::NotAuthenticated),
157            4004 => Some(CloseCode::AuthenticationFailed),
158            4005 => Some(CloseCode::AlreadyAuthenticated),
159            4007 => Some(CloseCode::InvalidSeq),
160            4008 => Some(CloseCode::RateLimited),
161            4009 => Some(CloseCode::SessionTimedOut),
162            4010 => Some(CloseCode::InvalidShard),
163            4011 => Some(CloseCode::ShardingRequired),
164            4012 => Some(CloseCode::InvalidApiVersion),
165            4013 => Some(CloseCode::InvalidIntents),
166            4014 => Some(CloseCode::DisallowedIntents),
167            _ => None,
168        }
169    }
170}