1use thiserror::Error;
2
3#[derive(Debug, Error)]
5#[non_exhaustive]
6pub enum CachekitError {
7 #[error("backend error: {0}")]
9 Backend(#[from] BackendError),
10
11 #[error("serialization error: {0}")]
13 Serialization(String),
14
15 #[error("encryption error: {0}")]
17 Encryption(String),
18
19 #[error("configuration error: {0}")]
21 Config(String),
22
23 #[error("payload too large: {size} bytes (limit: {limit} bytes)")]
25 PayloadTooLarge {
26 size: usize,
28 limit: usize,
30 },
31
32 #[error("invalid cache key: {0}")]
34 InvalidKey(String),
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum BackendErrorKind {
43 Transient,
45 Permanent,
47 Timeout,
49 Authentication,
51 CircuitOpen,
55 Backpressure,
60}
61
62impl BackendErrorKind {
63 #[must_use]
65 pub fn is_retryable(&self) -> bool {
66 matches!(self, Self::Transient | Self::Timeout)
67 }
68}
69
70impl std::fmt::Display for BackendErrorKind {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::Transient => write!(f, "transient"),
74 Self::Permanent => write!(f, "permanent"),
75 Self::Timeout => write!(f, "timeout"),
76 Self::Authentication => write!(f, "authentication"),
77 Self::CircuitOpen => write!(f, "circuit-open"),
78 Self::Backpressure => write!(f, "backpressure"),
79 }
80 }
81}
82
83#[derive(Debug, Error)]
87#[error("{kind} backend error: {message}")]
88pub struct BackendError {
89 pub kind: BackendErrorKind,
91 pub message: String,
93 #[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
95 #[source]
96 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
97 #[cfg(any(target_arch = "wasm32", feature = "unsync"))]
99 #[source]
100 pub source: Option<Box<dyn std::error::Error>>,
101}
102
103impl BackendError {
104 pub fn transient(message: impl Into<String>) -> Self {
106 Self {
107 kind: BackendErrorKind::Transient,
108 message: message.into(),
109 source: None,
110 }
111 }
112
113 pub fn permanent(message: impl Into<String>) -> Self {
115 Self {
116 kind: BackendErrorKind::Permanent,
117 message: message.into(),
118 source: None,
119 }
120 }
121
122 pub fn timeout(message: impl Into<String>) -> Self {
124 Self {
125 kind: BackendErrorKind::Timeout,
126 message: message.into(),
127 source: None,
128 }
129 }
130
131 pub fn auth(message: impl Into<String>) -> Self {
133 Self {
134 kind: BackendErrorKind::Authentication,
135 message: message.into(),
136 source: None,
137 }
138 }
139
140 pub fn circuit_open(message: impl Into<String>) -> Self {
142 Self {
143 kind: BackendErrorKind::CircuitOpen,
144 message: message.into(),
145 source: None,
146 }
147 }
148
149 pub fn backpressure(message: impl Into<String>) -> Self {
152 Self {
153 kind: BackendErrorKind::Backpressure,
154 message: message.into(),
155 source: None,
156 }
157 }
158
159 pub fn sanitize_message(msg: &str, api_key: &str) -> String {
161 if api_key.is_empty() {
162 return msg.to_string();
163 }
164 msg.replace(api_key, "***")
165 }
166
167 pub fn from_http_status(status: u16, body: &[u8]) -> Self {
171 let body_str = std::str::from_utf8(body).unwrap_or("<non-utf8 body>");
172 let truncated: String = body_str.chars().take(256).collect();
173 let message = format!("HTTP {status}: {truncated}");
174
175 let kind = match status {
176 401 | 403 => BackendErrorKind::Authentication,
177 408 | 429 | 500 | 502 | 503 | 504 => BackendErrorKind::Transient,
178 _ if status >= 500 => BackendErrorKind::Transient,
179 _ => BackendErrorKind::Permanent,
180 };
181
182 Self {
183 kind,
184 message,
185 source: None,
186 }
187 }
188}