Skip to main content

steam_user/
error.rs

1//! Error types for Steam Community operations.
2
3use thiserror::Error;
4
5/// Errors that can occur during Steam Community operations.
6#[derive(Debug, Error)]
7pub enum SteamUserError {
8    /// Not logged in to Steam Community.
9    #[error("Not logged in")]
10    NotLoggedIn,
11
12    /// Session has expired and needs to be refreshed.
13    #[error("Session expired")]
14    SessionExpired,
15
16    /// Steam accepted the renewal request (HTTP 200) but returned no new
17    /// access token. This means the refresh token is no longer valid for
18    /// renewal (server-side revoked or expired), even if its JWT `exp` is
19    /// still in the future. Callers must perform a full re-login instead of
20    /// retrying renewal — a retry yields the same empty result.
21    #[error("Renewal rejected: Steam returned no access token (refresh token no longer renewable)")]
22    RenewalRejected,
23
24    /// Family View is restricting access.
25    #[error("Family View restricted")]
26    FamilyViewRestricted,
27
28    /// Account is limited (e.g. no games) and cannot access Web API.
29    #[error("Account is limited: {0}")]
30    LimitedAccount(String),
31
32    /// HTTP request failed.
33    #[error("HTTP error: {0}")]
34    HttpError(#[from] reqwest::Error),
35
36    /// Invalid response from Steam.
37    #[error("Malformed response: {0}")]
38    MalformedResponse(String),
39
40    /// Steam returned an error.
41    #[error("Steam error: {0}")]
42    SteamError(String),
43
44    /// Steam returned an EResult error code.
45    #[error("EResult {code}: {message}")]
46    EResult {
47        /// The error code.
48        code: i32,
49        /// Human-readable message.
50        message: String,
51    },
52
53    /// Invalid confirmation key.
54    #[error("Invalid confirmation key")]
55    InvalidConfirmationKey,
56
57    /// Confirmation not found.
58    #[error("Confirmation not found for object {0}")]
59    ConfirmationNotFound(u64),
60
61    /// Invalid 2FA setup state.
62    #[error("2FA error: {0}")]
63    TwoFactorError(String),
64
65    /// Invalid image format.
66    #[error("Invalid image format: {0}")]
67    InvalidImageFormat(String),
68
69    /// Rate limited by Steam.
70    #[error("Rate limited")]
71    RateLimited,
72
73    /// A required credential (token/secret) is missing.
74    #[error("Missing credential: {field}")]
75    MissingCredential {
76        /// The name of the missing field (e.g. "access_token",
77        /// "refresh_token").
78        field: &'static str,
79    },
80
81    /// HTTP request returned a non-success status code.
82    #[error("HTTP {status} from {url}")]
83    HttpStatus {
84        /// The HTTP status code.
85        status: u16,
86        /// The URL that returned the error.
87        url: String,
88    },
89
90    /// Failed to build the HTTP client.
91    #[error("HTTP client build failed: {0}")]
92    ClientBuild(String),
93
94    /// Redirect handling error (loop, missing Location header, too many hops).
95    #[error("Redirect error: {0}")]
96    RedirectError(String),
97
98    /// Invalid or malformed input parameter.
99    #[error("Invalid input: {0}")]
100    InvalidInput(String),
101
102    /// Protobuf encoding error.
103    #[error("Protobuf encode error: {0}")]
104    ProtobufEncode(#[from] prost::EncodeError),
105
106    /// Protobuf decoding error.
107    #[error("Protobuf decode error: {0}")]
108    ProtobufDecode(#[from] prost::DecodeError),
109
110    /// URL parsing error.
111    #[error("URL error: {0}")]
112    UrlError(#[from] url::ParseError),
113
114    /// JSON parsing error.
115    #[error("JSON error: {0}")]
116    JsonError(#[from] serde_json::Error),
117
118    /// Base64 decoding error.
119    #[error("Base64 error: {0}")]
120    Base64Error(#[from] base64::DecodeError),
121
122    /// I/O error (filesystem, OS-level).
123    #[error("I/O error: {0}")]
124    Io(#[from] std::io::Error),
125
126    /// SystemTime error (clock went backwards before UNIX epoch).
127    #[error("System time error: {0}")]
128    SystemTime(#[from] std::time::SystemTimeError),
129
130    /// Other error.
131    #[error("{0}")]
132    Other(String),
133
134    /// Error from the remote (`steam-user-api`) client.
135    #[cfg(feature = "remote")]
136    #[error(transparent)]
137    RemoteFailed(#[from] Box<crate::remote::RemoteSteamUserError>),
138
139    /// Error from the GAS client.
140    #[cfg(feature = "gas")]
141    #[error(transparent)]
142    GasFailed(#[from] Box<crate::gas::GasError>),
143
144    /// TOTP generation error.
145    #[error("TOTP error: {0}")]
146    Totp(#[from] steam_totp::TotpError),
147
148    /// Middleware error from reqwest-middleware.
149    /// Note: We use anyhow::Error here because
150    /// reqwest-middleware::Error::Middleware wraps anyhow::Error, and we
151    /// want to preserve the full error chain.
152    #[error("Middleware error: {0:#}")]
153    Middleware(anyhow::Error),
154
155    /// An error that occurred while performing a specific API action.
156    #[error("Failed to execute {action:?}: {source}")]
157    ActionFailed {
158        /// The action that failed
159        action: crate::action::ApiAction,
160        /// The underlying error
161        #[source]
162        source: Box<SteamUserError>,
163    },
164}
165
166impl From<reqwest_middleware::Error> for SteamUserError {
167    fn from(err: reqwest_middleware::Error) -> Self {
168        match err {
169            reqwest_middleware::Error::Reqwest(e) => Self::HttpError(e),
170            reqwest_middleware::Error::Middleware(e) => Self::Middleware(e),
171        }
172    }
173}
174
175impl SteamUserError {
176    /// Check if this error was wrapped with an API action context.
177    pub fn api_action(&self) -> Option<crate::action::ApiAction> {
178        match self {
179            Self::ActionFailed { action, .. } => Some(*action),
180            _ => None,
181        }
182    }
183
184    /// Create an EResult error from a code.
185    pub fn from_eresult(code: i32) -> Self {
186        let message = steam_enums::eresult::EResult::from_i32(code).map(|e| format!("{e:?}")).unwrap_or_else(|| format!("Unknown({code})"));
187        Self::EResult { code, message }
188    }
189
190    /// Check if eresult code is OK (1).
191    pub fn check_eresult(code: i32) -> Result<(), Self> {
192        if code == 1 {
193            Ok(())
194        } else {
195            Err(Self::from_eresult(code))
196        }
197    }
198
199    /// Returns `true` if the error is likely transient and safe to retry.
200    pub fn is_retryable(&self) -> bool {
201        match self {
202            Self::ActionFailed { action, source } => action.is_read_only() && source.is_retryable(),
203            Self::RateLimited => true,
204            Self::HttpStatus { status, .. } => *status == 429 || *status >= 500,
205            Self::HttpError(e) => e.is_connect() || e.is_timeout(),
206            Self::Middleware(_) => true,
207            #[cfg(feature = "remote")]
208            Self::RemoteFailed(e) => matches!(
209                e.as_ref(),
210                crate::remote::RemoteSteamUserError::Http(_)
211                    | crate::remote::RemoteSteamUserError::AllRetriesFailed
212            ),
213            #[cfg(feature = "gas")]
214            Self::GasFailed(e) => matches!(
215                e.as_ref(),
216                crate::gas::GasError::Http(_) | crate::gas::GasError::AllRetriesFailed
217            ),
218            _ => false,
219        }
220    }
221}