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