1use steam_auth::EAuthSessionGuardType;
4use steam_cm_provider::CmError;
5use steam_enums::EResult;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
10#[non_exhaustive]
11pub enum SteamError {
12 #[error("Steam error: {0:?}")]
14 SteamResult(EResult),
15
16 #[error("Connection error: {0}")]
18 ConnectionError(String),
19
20 #[error("Already logged on")]
22 AlreadyLoggedOn,
23
24 #[error("Already connecting")]
26 AlreadyConnecting,
27
28 #[error("Not logged on")]
30 NotLoggedOn,
31
32 #[error("Not connected")]
34 NotConnected,
35
36 #[error("Invalid credentials")]
38 InvalidCredentials,
39
40 #[error("Steam Guard required: {guard_type:?}")]
48 SteamGuardRequired {
49 guard_type: EAuthSessionGuardType,
51 email_domain: Option<String>,
53 },
54
55 #[error("Two-factor authentication required")]
57 TwoFactorRequired,
58
59 #[error("Invalid token: {0}")]
61 InvalidToken(String),
62
63 #[error("Network error: {0}")]
65 NetworkError(std::io::Error),
66
67 #[error("Operation timed out")]
69 Timeout,
70
71 #[error("Response timed out")]
73 ResponseTimeout,
74
75 #[error("Deserialization failed")]
77 DeserializationFailed,
78
79 #[error("Protocol error: {0}")]
81 ProtocolError(String),
82
83 #[error("Bad response: {message}")]
88 BadResponse {
89 message: String,
91 emsg: Option<steam_enums::EMsg>,
93 raw_bytes: Option<Vec<u8>>,
95 },
96
97 #[error("Session error: {0}")]
99 SessionError(#[from] steam_auth::SessionError),
100
101 #[error("Not implemented: {0}")]
103 NotImplemented(String),
104
105 #[error(transparent)]
107 Reqwest(#[from] reqwest::Error),
108
109 #[error(transparent)]
111 WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
112
113 #[error(transparent)]
115 Decode(#[from] prost::DecodeError),
116
117 #[error("{0}")]
120 Other(String),
121}
122
123impl SteamError {
124 pub fn eresult(&self) -> Option<EResult> {
126 match self {
127 SteamError::SteamResult(result) => Some(*result),
128 _ => None,
129 }
130 }
131
132 pub fn is_retryable(&self) -> bool {
141 match self {
142 SteamError::SteamResult(result) => matches!(result, EResult::Fail | EResult::ServiceUnavailable | EResult::TryAnotherCM | EResult::NoConnection),
143 SteamError::NetworkError(_) | SteamError::Timeout => true,
144 _ => false,
145 }
146 }
147
148 pub fn bad_response(message: impl Into<String>) -> Self {
151 SteamError::BadResponse { message: message.into(), emsg: None, raw_bytes: None }
152 }
153
154 pub fn bad_response_with_context(message: impl Into<String>, emsg: Option<steam_enums::EMsg>, raw_bytes: Option<Vec<u8>>) -> Self {
156 SteamError::BadResponse { message: message.into(), emsg, raw_bytes }
157 }
158}
159
160impl From<CmError> for SteamError {
161 fn from(e: CmError) -> Self {
162 match e {
163 CmError::Network(s) => SteamError::NetworkError(std::io::Error::other(s)),
164 CmError::Protocol(s) => SteamError::ProtocolError(s),
165 CmError::ApiError(status, msg) => SteamError::ProtocolError(format!("Steam API error (status {}): {}", status, msg)),
166 CmError::InvalidResponse(s) => SteamError::bad_response(s),
167 CmError::Connection(s) => SteamError::ConnectionError(s),
168 CmError::CacheError(s) => SteamError::Other(format!("Cache error: {}", s)),
169 CmError::Timeout => SteamError::Timeout,
170 CmError::NoServers => SteamError::ConnectionError("No CM servers available".into()),
171 CmError::Io(e) => SteamError::NetworkError(e),
172 CmError::Json(e) => SteamError::ProtocolError(format!("JSON error: {}", e)),
173 CmError::Other(s) => SteamError::Other(s),
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::error::Error as _;
182
183 #[test]
193 fn prost_decode_error_preserves_source_chain() {
194 let decode_err = prost::DecodeError::new("invalid wire format");
195 let expected_display = decode_err.to_string();
196
197 let steam_err: SteamError = decode_err.into();
198
199 let inner = match &steam_err {
201 SteamError::Decode(e) => e,
202 other => panic!("expected SteamError::Decode, got {:?}", other),
203 };
204
205 assert_eq!(steam_err.to_string(), expected_display);
207 assert_eq!(inner.to_string(), expected_display);
208
209 #[derive(Debug)]
213 struct Wrap(SteamError);
214 impl std::fmt::Display for Wrap {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(f, "wrapped")
217 }
218 }
219 impl std::error::Error for Wrap {
220 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
221 Some(&self.0)
222 }
223 }
224 let wrapped = Wrap(steam_err);
225 let src = wrapped.source().expect("wrapper exposes SteamError as source");
226 assert!(src.downcast_ref::<SteamError>().is_some(), "source should downcast to SteamError");
230 }
231
232 #[test]
236 fn prost_message_decode_failure_converts_via_from() {
237 use prost::Message;
238
239 let bad: &[u8] = &[0xff, 0xff, 0xff];
240 let result: Result<String, prost::DecodeError> = String::decode(bad);
241 let decode_err = result.expect_err("decoding malformed bytes must fail");
242 let expected_display = decode_err.to_string();
243
244 let steam_err: SteamError = decode_err.into();
245 assert!(matches!(steam_err, SteamError::Decode(_)));
246 assert_eq!(steam_err.to_string(), expected_display);
249 }
250}