grok_api 0.1.6

Rust client library for the Grok AI API (xAI)
Documentation
//! Error types for the Grok API client

/// Result type alias for Grok API operations
pub type Result<T> = std::result::Result<T, Error>;

/// Main error type for the Grok API client
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// Network error (connection issues, timeouts, etc.)
    #[error("Network error: {0}")]
    Network(String),

    /// HTTP error with status code
    #[error("HTTP error {status}: {message}")]
    Http { status: u16, message: String },

    /// Authentication failed (invalid API key)
    #[error("Authentication failed: Invalid or missing API key")]
    Authentication,

    /// Rate limit exceeded
    #[error("Rate limit exceeded. Please try again later")]
    RateLimit,

    /// Model not found or not available
    #[error("Model not found: {0}")]
    ModelNotFound(String),

    /// Invalid request (bad parameters, malformed request, etc.)
    #[error("Invalid request: {0}")]
    InvalidRequest(String),

    /// Server error (5xx responses)
    #[error("Server error: {0}")]
    ServerError(String),

    /// JSON serialization/deserialization error
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    /// Request timeout
    #[error("Request timeout after {0} seconds")]
    Timeout(u64),

    /// Connection dropped (Starlink-specific)
    #[error("Connection dropped - possible network instability")]
    ConnectionDropped,

    /// Maximum retries exceeded
    #[error("Maximum retries exceeded ({0} attempts)")]
    MaxRetriesExceeded(u32),

    /// Empty or invalid API key
    #[error("API key cannot be empty")]
    EmptyApiKey,

    /// Invalid configuration
    #[error("Invalid configuration: {0}")]
    InvalidConfig(String),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

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

impl Error {
    /// Check if the error is retryable
    pub fn is_retryable(&self) -> bool {
        matches!(
            self,
            Error::Network(_)
                | Error::Timeout(_)
                | Error::ConnectionDropped
                | Error::Http {
                    status: 502 | 503 | 504 | 520 | 521 | 522 | 523 | 524,
                    ..
                }
        )
    }

    /// Check if the error is a rate limit error
    pub fn is_rate_limit(&self) -> bool {
        matches!(self, Error::RateLimit)
    }

    /// Check if the error is an authentication error
    pub fn is_auth_error(&self) -> bool {
        matches!(self, Error::Authentication)
    }

    /// Check if the error indicates a Starlink network drop
    pub fn is_starlink_drop(&self) -> bool {
        match self {
            Error::ConnectionDropped => true,
            Error::Network(msg) => {
                let msg_lower = msg.to_lowercase();
                msg_lower.contains("connection reset")
                    || msg_lower.contains("broken pipe")
                    || msg_lower.contains("network unreachable")
            }
            _ => false,
        }
    }

    /// Convert from reqwest::Error
    pub fn from_reqwest(err: reqwest::Error) -> Self {
        let error_string = err.to_string();
        let error_lower = error_string.to_lowercase();

        // Check for timeout
        if err.is_timeout() || error_lower.contains("timeout") {
            return Error::Timeout(30); // Default timeout value
        }

        // Check for connection issues
        if err.is_connect() || error_lower.contains("connection") {
            return Error::Network(error_string);
        }

        // Check for status codes
        if let Some(status) = err.status() {
            let code = status.as_u16();
            return match code {
                401 => Error::Authentication,
                429 => Error::RateLimit,
                404 => Error::InvalidRequest("Endpoint not found".to_string()),
                400 => Error::InvalidRequest(error_string),
                500..=599 => Error::ServerError(error_string),
                _ => Error::Http {
                    status: code,
                    message: error_string,
                },
            };
        }

        // Default to network error
        Error::Network(error_string)
    }
}

impl From<reqwest::Error> for Error {
    fn from(err: reqwest::Error) -> Self {
        Error::from_reqwest(err)
    }
}

impl From<anyhow::Error> for Error {
    fn from(err: anyhow::Error) -> Self {
        Error::Other(err.to_string())
    }
}

impl From<String> for Error {
    fn from(s: String) -> Self {
        Error::Other(s)
    }
}

impl From<&str> for Error {
    fn from(s: &str) -> Self {
        Error::Other(s.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_is_retryable() {
        assert!(Error::Network("connection failed".to_string()).is_retryable());
        assert!(Error::Timeout(30).is_retryable());
        assert!(Error::ConnectionDropped.is_retryable());
        assert!(!Error::Authentication.is_retryable());
        assert!(!Error::RateLimit.is_retryable());
    }

    #[test]
    fn test_error_is_starlink_drop() {
        assert!(Error::ConnectionDropped.is_starlink_drop());
        assert!(Error::Network("connection reset by peer".to_string()).is_starlink_drop());
        assert!(Error::Network("broken pipe".to_string()).is_starlink_drop());
        assert!(!Error::Authentication.is_starlink_drop());
    }

    #[test]
    fn test_error_display() {
        let err = Error::Authentication;
        assert_eq!(
            err.to_string(),
            "Authentication failed: Invalid or missing API key"
        );

        let err = Error::ModelNotFound("grok-99".to_string());
        assert_eq!(err.to_string(), "Model not found: grok-99");
    }
}