Skip to main content

trailcache_core/api/
error.rs

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum ApiError {
5    #[error("Access denied: {0}")]
6    AccessDenied(String),
7
8    #[error("Unauthorized - token may be expired")]
9    Unauthorized,
10
11    #[error("Resource not found: {0}")]
12    NotFound(String),
13
14    #[error("Rate limited - please wait before retrying")]
15    RateLimited,
16
17    #[error("Server error ({1}): {0}")]
18    ServerError(String, u16),
19
20    #[error("Network error: {0}")]
21    NetworkError(#[from] reqwest::Error),
22
23    #[error("Invalid response: {0}")]
24    InvalidResponse(String),
25}
26
27/// Maximum length for error response bodies in error messages
28const MAX_ERROR_BODY_LENGTH: usize = 500;
29
30impl ApiError {
31    /// Truncate a response body to avoid logging excessive data
32    fn truncate_body(body: &str) -> String {
33        if body.len() <= MAX_ERROR_BODY_LENGTH {
34            body.to_string()
35        } else {
36            format!("{}... (truncated, {} total bytes)",
37                    &body[..MAX_ERROR_BODY_LENGTH],
38                    body.len())
39        }
40    }
41
42    pub fn from_status(status: reqwest::StatusCode, body: &str) -> Self {
43        let truncated = Self::truncate_body(body);
44        match status.as_u16() {
45            401 => ApiError::Unauthorized,
46            403 => ApiError::AccessDenied(truncated),
47            404 => ApiError::NotFound(truncated),
48            429 => ApiError::RateLimited,
49            500..=599 => ApiError::ServerError(truncated, status.as_u16()),
50            _ => ApiError::InvalidResponse(format!("Status {}: {}", status, truncated)),
51        }
52    }
53}