steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Error types for Steam Community operations.

use thiserror::Error;

/// Errors that can occur during Steam Community operations.
#[derive(Debug, Error)]
pub enum SteamUserError {
    /// Not logged in to Steam Community.
    #[error("Not logged in")]
    NotLoggedIn,

    /// Session has expired and needs to be refreshed.
    #[error("Session expired")]
    SessionExpired,

    /// Family View is restricting access.
    #[error("Family View restricted")]
    FamilyViewRestricted,

    /// Account is limited (e.g. no games) and cannot access Web API.
    #[error("Account is limited: {0}")]
    LimitedAccount(String),

    /// HTTP request failed.
    #[error("HTTP error: {0}")]
    HttpError(#[from] reqwest::Error),

    /// Invalid response from Steam.
    #[error("Malformed response: {0}")]
    MalformedResponse(String),

    /// Steam returned an error.
    #[error("Steam error: {0}")]
    SteamError(String),

    /// Steam returned an EResult error code.
    #[error("EResult {code}: {message}")]
    EResult {
        /// The error code.
        code: i32,
        /// Human-readable message.
        message: String,
    },

    /// Invalid confirmation key.
    #[error("Invalid confirmation key")]
    InvalidConfirmationKey,

    /// Confirmation not found.
    #[error("Confirmation not found for object {0}")]
    ConfirmationNotFound(u64),

    /// Invalid 2FA setup state.
    #[error("2FA error: {0}")]
    TwoFactorError(String),

    /// Invalid image format.
    #[error("Invalid image format: {0}")]
    InvalidImageFormat(String),

    /// Rate limited by Steam.
    #[error("Rate limited")]
    RateLimited,

    /// A required credential (token/secret) is missing.
    #[error("Missing credential: {field}")]
    MissingCredential {
        /// The name of the missing field (e.g. "access_token",
        /// "refresh_token").
        field: &'static str,
    },

    /// HTTP request returned a non-success status code.
    #[error("HTTP {status} from {url}")]
    HttpStatus {
        /// The HTTP status code.
        status: u16,
        /// The URL that returned the error.
        url: String,
    },

    /// Failed to build the HTTP client.
    #[error("HTTP client build failed: {0}")]
    ClientBuild(String),

    /// Redirect handling error (loop, missing Location header, too many hops).
    #[error("Redirect error: {0}")]
    RedirectError(String),

    /// Invalid or malformed input parameter.
    #[error("Invalid input: {0}")]
    InvalidInput(String),

    /// Protobuf encoding error.
    #[error("Protobuf encode error: {0}")]
    ProtobufEncode(#[from] prost::EncodeError),

    /// Protobuf decoding error.
    #[error("Protobuf decode error: {0}")]
    ProtobufDecode(#[from] prost::DecodeError),

    /// URL parsing error.
    #[error("URL error: {0}")]
    UrlError(#[from] url::ParseError),

    /// JSON parsing error.
    #[error("JSON error: {0}")]
    JsonError(#[from] serde_json::Error),

    /// Base64 decoding error.
    #[error("Base64 error: {0}")]
    Base64Error(#[from] base64::DecodeError),

    /// I/O error (filesystem, OS-level).
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    /// SystemTime error (clock went backwards before UNIX epoch).
    #[error("System time error: {0}")]
    SystemTime(#[from] std::time::SystemTimeError),

    /// Other error.
    #[error("{0}")]
    Other(String),

    /// Error from the remote (`steam-user-api`) client.
    #[cfg(feature = "remote")]
    #[error(transparent)]
    RemoteFailed(#[from] Box<crate::remote::RemoteSteamUserError>),

    /// Error from the GAS client.
    #[cfg(feature = "gas")]
    #[error(transparent)]
    GasFailed(#[from] Box<crate::gas::GasError>),

    /// TOTP generation error.
    #[error("TOTP error: {0}")]
    Totp(#[from] steam_totp::TotpError),

    /// Middleware error from reqwest-middleware.
    /// Note: We use anyhow::Error here because
    /// reqwest-middleware::Error::Middleware wraps anyhow::Error, and we
    /// want to preserve the full error chain.
    #[error("Middleware error: {0:#}")]
    Middleware(anyhow::Error),

    /// An error that occurred while performing a specific API action.
    #[error("Failed to execute {action:?}: {source}")]
    ActionFailed {
        /// The action that failed
        action: crate::action::ApiAction,
        /// The underlying error
        #[source]
        source: Box<SteamUserError>,
    },
}

impl From<reqwest_middleware::Error> for SteamUserError {
    fn from(err: reqwest_middleware::Error) -> Self {
        match err {
            reqwest_middleware::Error::Reqwest(e) => Self::HttpError(e),
            reqwest_middleware::Error::Middleware(e) => Self::Middleware(e),
        }
    }
}

impl SteamUserError {
    /// Check if this error was wrapped with an API action context.
    pub fn api_action(&self) -> Option<crate::action::ApiAction> {
        match self {
            Self::ActionFailed { action, .. } => Some(*action),
            _ => None,
        }
    }

    /// Create an EResult error from a code.
    pub fn from_eresult(code: i32) -> Self {
        let message = steam_enums::eresult::EResult::from_i32(code).map(|e| format!("{e:?}")).unwrap_or_else(|| format!("Unknown({code})"));
        Self::EResult { code, message }
    }

    /// Check if eresult code is OK (1).
    pub fn check_eresult(code: i32) -> Result<(), Self> {
        if code == 1 {
            Ok(())
        } else {
            Err(Self::from_eresult(code))
        }
    }

    /// Returns `true` if the error is likely transient and safe to retry.
    pub fn is_retryable(&self) -> bool {
        match self {
            Self::ActionFailed { action, source } => action.is_read_only() && source.is_retryable(),
            Self::RateLimited => true,
            Self::HttpStatus { status, .. } => *status == 429 || *status >= 500,
            Self::HttpError(e) => e.is_connect() || e.is_timeout(),
            Self::Middleware(_) => true,
            #[cfg(feature = "remote")]
            Self::RemoteFailed(e) => matches!(
                e.as_ref(),
                crate::remote::RemoteSteamUserError::Http(_)
                    | crate::remote::RemoteSteamUserError::AllRetriesFailed
            ),
            #[cfg(feature = "gas")]
            Self::GasFailed(e) => matches!(
                e.as_ref(),
                crate::gas::GasError::Http(_) | crate::gas::GasError::AllRetriesFailed
            ),
            _ => false,
        }
    }
}