Skip to main content

steam_client/
error.rs

1//! Steam client error types.
2
3use steam_auth::EAuthSessionGuardType;
4use steam_cm_provider::CmError;
5use steam_enums::EResult;
6use thiserror::Error;
7
8/// Errors that can occur when using the Steam client.
9#[derive(Error, Debug)]
10pub enum SteamError {
11    /// Steam returned an error result.
12    #[error("Steam error: {0:?}")]
13    SteamResult(EResult),
14
15    /// Connection error.
16    #[error("Connection error: {0}")]
17    ConnectionError(String),
18
19    /// Already logged on.
20    #[error("Already logged on")]
21    AlreadyLoggedOn,
22
23    /// Already connecting.
24    #[error("Already connecting")]
25    AlreadyConnecting,
26
27    /// Not logged on.
28    #[error("Not logged on")]
29    NotLoggedOn,
30
31    /// Not connected.
32    #[error("Not connected")]
33    NotConnected,
34
35    /// Invalid credentials.
36    #[error("Invalid credentials")]
37    InvalidCredentials,
38
39    /// Steam Guard authentication required.
40    ///
41    /// This error is returned when password authentication requires a Steam
42    /// Guard code. The `guard_type` indicates what kind of code is needed:
43    /// - `EmailCode`: A code sent to the account's email
44    /// - `DeviceCode`: A TOTP code from the Steam mobile app
45    /// - `DeviceConfirmation`: Approval via the Steam mobile app
46    #[error("Steam Guard required: {guard_type:?}")]
47    SteamGuardRequired {
48        /// Type of Steam Guard verification needed.
49        guard_type: EAuthSessionGuardType,
50        /// Email domain hint (e.g., "g****.com") if guard_type is EmailCode.
51        email_domain: Option<String>,
52    },
53
54    /// Two-factor authentication required (legacy).
55    #[error("Two-factor authentication required")]
56    TwoFactorRequired,
57
58    /// Invalid token.
59    #[error("Invalid token: {0}")]
60    InvalidToken(String),
61
62    /// Network error.
63    #[error("Network error: {0}")]
64    NetworkError(std::io::Error),
65
66    /// Timeout.
67    #[error("Operation timed out")]
68    Timeout,
69
70    /// Response timed out.
71    #[error("Response timed out")]
72    ResponseTimeout,
73
74    /// Deserialization failed.
75    #[error("Deserialization failed")]
76    DeserializationFailed,
77
78    /// Protocol error.
79    #[error("Protocol error: {0}")]
80    ProtocolError(String),
81
82    /// Bad response from Steam.
83    ///
84    /// This error is returned when Steam returns a malformed response or one
85    /// that violates expectations (e.g. missing SteamID in logon response).
86    #[error("Bad response: {message}")]
87    BadResponse {
88        /// Human-readable error message.
89        message: String,
90        /// The EMsg that triggered this error (if known).
91        emsg: Option<steam_enums::EMsg>,
92        /// The raw bytes that failed to parse (truncated for display).
93        raw_bytes: Option<Vec<u8>>,
94    },
95
96    /// Session error.
97    #[error("Session error: {0}")]
98    SessionError(#[from] steam_auth::SessionError),
99
100    /// Not implemented yet.
101    #[error("Not implemented: {0}")]
102    NotImplemented(String),
103
104    /// Other error.
105    #[error("{0}")]
106    Other(String),
107}
108
109impl SteamError {
110    /// Returns the EResult if this is a Steam result error.
111    pub fn eresult(&self) -> Option<EResult> {
112        match self {
113            SteamError::SteamResult(result) => Some(*result),
114            _ => None,
115        }
116    }
117
118    /// Returns true if the error is a transient error that might be resolved by
119    /// retrying.
120    ///
121    /// Matches Node.js behavior for handling:
122    /// - Fail
123    /// - ServiceUnavailable
124    /// - TryAnotherCM
125    /// - NoConnection (in logoff context)
126    pub fn is_retryable(&self) -> bool {
127        match self {
128            SteamError::SteamResult(result) => matches!(result, EResult::Fail | EResult::ServiceUnavailable | EResult::TryAnotherCM | EResult::NoConnection),
129            SteamError::NetworkError(_) | SteamError::Timeout => true,
130            _ => false,
131        }
132    }
133
134    /// Create a BadResponse error with just a message (backwards-compatible
135    /// shorthand).
136    pub fn bad_response(message: impl Into<String>) -> Self {
137        SteamError::BadResponse { message: message.into(), emsg: None, raw_bytes: None }
138    }
139
140    /// Create a BadResponse error with full context.
141    pub fn bad_response_with_context(message: impl Into<String>, emsg: Option<steam_enums::EMsg>, raw_bytes: Option<Vec<u8>>) -> Self {
142        SteamError::BadResponse { message: message.into(), emsg, raw_bytes }
143    }
144}
145
146impl From<CmError> for SteamError {
147    fn from(e: CmError) -> Self {
148        match e {
149            CmError::Network(s) => SteamError::NetworkError(std::io::Error::other(s)),
150            CmError::Protocol(s) => SteamError::ProtocolError(s),
151            CmError::ApiError(status, msg) => SteamError::ProtocolError(format!("Steam API error (status {}): {}", status, msg)),
152            CmError::InvalidResponse(s) => SteamError::bad_response(s),
153            CmError::Connection(s) => SteamError::ConnectionError(s),
154            CmError::CacheError(s) => SteamError::Other(format!("Cache error: {}", s)),
155            CmError::Timeout => SteamError::Timeout,
156            CmError::NoServers => SteamError::ConnectionError("No CM servers available".into()),
157            CmError::Io(e) => SteamError::NetworkError(e),
158            CmError::Json(e) => SteamError::ProtocolError(format!("JSON error: {}", e)),
159            CmError::Other(s) => SteamError::Other(s),
160        }
161    }
162}