Skip to main content

cachekit/
error.rs

1use thiserror::Error;
2
3/// Top-level error type for all CacheKit operations.
4#[derive(Debug, Error)]
5#[non_exhaustive]
6pub enum CachekitError {
7    /// An error originating from the cache backend.
8    #[error("backend error: {0}")]
9    Backend(#[from] BackendError),
10
11    /// Serialization or deserialization failed.
12    #[error("serialization error: {0}")]
13    Serialization(String),
14
15    /// Encryption or decryption failed.
16    #[error("encryption error: {0}")]
17    Encryption(String),
18
19    /// Configuration is invalid or missing required values.
20    #[error("configuration error: {0}")]
21    Config(String),
22
23    /// The payload exceeds the maximum allowed size.
24    #[error("payload too large: {size} bytes (limit: {limit} bytes)")]
25    PayloadTooLarge {
26        /// Actual payload size in bytes.
27        size: usize,
28        /// Maximum allowed size in bytes.
29        limit: usize,
30    },
31
32    /// The cache key is invalid (empty, too long, or contains illegal bytes).
33    #[error("invalid cache key: {0}")]
34    InvalidKey(String),
35}
36
37// ── BackendErrorKind ─────────────────────────────────────────────────────────
38
39/// Classifies backend errors to determine retry behaviour.
40#[derive(Debug, Clone, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum BackendErrorKind {
43    /// Temporary failure — safe to retry (network blip, pool exhaustion).
44    Transient,
45    /// Permanent failure — retrying will not help (bad request, key not found).
46    Permanent,
47    /// Request did not complete within the deadline — safe to retry.
48    Timeout,
49    /// Credentials are invalid or missing — retrying will not help.
50    Authentication,
51}
52
53impl BackendErrorKind {
54    /// Returns `true` if it is safe to retry the operation.
55    #[must_use]
56    pub fn is_retryable(&self) -> bool {
57        matches!(self, Self::Transient | Self::Timeout)
58    }
59}
60
61impl std::fmt::Display for BackendErrorKind {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::Transient => write!(f, "transient"),
65            Self::Permanent => write!(f, "permanent"),
66            Self::Timeout => write!(f, "timeout"),
67            Self::Authentication => write!(f, "authentication"),
68        }
69    }
70}
71
72// ── BackendError ─────────────────────────────────────────────────────────────
73
74/// A structured error from a cache backend.
75#[derive(Debug, Error)]
76#[error("{kind} backend error: {message}")]
77pub struct BackendError {
78    /// Classification of this error.
79    pub kind: BackendErrorKind,
80    /// Human-readable description.
81    pub message: String,
82    /// The underlying error that caused this backend error, if any.
83    #[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
84    #[source]
85    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
86    /// The underlying error that caused this backend error, if any.
87    #[cfg(any(target_arch = "wasm32", feature = "unsync"))]
88    #[source]
89    pub source: Option<Box<dyn std::error::Error>>,
90}
91
92impl BackendError {
93    /// Create a transient (retryable) backend error.
94    pub fn transient(message: impl Into<String>) -> Self {
95        Self {
96            kind: BackendErrorKind::Transient,
97            message: message.into(),
98            source: None,
99        }
100    }
101
102    /// Create a permanent (non-retryable) backend error.
103    pub fn permanent(message: impl Into<String>) -> Self {
104        Self {
105            kind: BackendErrorKind::Permanent,
106            message: message.into(),
107            source: None,
108        }
109    }
110
111    /// Create a timeout backend error.
112    pub fn timeout(message: impl Into<String>) -> Self {
113        Self {
114            kind: BackendErrorKind::Timeout,
115            message: message.into(),
116            source: None,
117        }
118    }
119
120    /// Create an authentication backend error.
121    pub fn auth(message: impl Into<String>) -> Self {
122        Self {
123            kind: BackendErrorKind::Authentication,
124            message: message.into(),
125            source: None,
126        }
127    }
128
129    /// Sanitize error messages to strip API keys (CWE-532).
130    pub fn sanitize_message(msg: &str, api_key: &str) -> String {
131        if api_key.is_empty() {
132            return msg.to_string();
133        }
134        msg.replace(api_key, "***")
135    }
136
137    /// Construct a [`BackendError`] from an HTTP status code and response body.
138    ///
139    /// The body is truncated to 256 Unicode scalar values to avoid inflating error messages.
140    pub fn from_http_status(status: u16, body: &[u8]) -> Self {
141        let body_str = std::str::from_utf8(body).unwrap_or("<non-utf8 body>");
142        let truncated: String = body_str.chars().take(256).collect();
143        let message = format!("HTTP {status}: {truncated}");
144
145        let kind = match status {
146            401 | 403 => BackendErrorKind::Authentication,
147            408 | 429 | 500 | 502 | 503 | 504 => BackendErrorKind::Transient,
148            _ if status >= 500 => BackendErrorKind::Transient,
149            _ => BackendErrorKind::Permanent,
150        };
151
152        Self {
153            kind,
154            message,
155            source: None,
156        }
157    }
158}