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