1use axum::{Json, http::StatusCode, response::IntoResponse};
7use tracing::warn;
8
9use crate::types::ErrorResponse;
10
11pub type ClResult<T> = std::result::Result<T, Error>;
12
13#[derive(Debug)]
14pub enum Error {
15 NotFound,
17 Gone,
22 PermissionDenied,
23 Unauthorized, DbError,
25 Parse,
26
27 ValidationError(String), Conflict(String), PreconditionRequired(String), SettingNotFound(String),
37
38 NetworkError(String), Timeout, ConfigError(String), ServiceUnavailable(String), Internal(String), ImageError(String), CryptoError(String), FileSourceNotFound, FileSourceForbidden, FileSourceUnreachable, FileCycleRejected, Io(std::io::Error),
59}
60
61impl From<std::io::Error> for Error {
62 fn from(err: std::io::Error) -> Self {
63 warn!("io error: {}", err);
64 Self::Io(err)
65 }
66}
67
68impl Error {
69 pub fn is_retryable(&self) -> bool {
88 !matches!(
89 self,
90 Error::PermissionDenied
91 | Error::Unauthorized
92 | Error::ValidationError(_)
93 | Error::Parse
94 | Error::Gone
95 )
96 }
97}
98
99impl std::fmt::Display for Error {
100 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
101 write!(f, "{:?}", self)
102 }
103}
104
105impl std::error::Error for Error {}
106
107impl IntoResponse for Error {
108 fn into_response(self) -> axum::response::Response {
109 let (status, code, message) = match self {
110 Error::NotFound => (
111 StatusCode::NOT_FOUND,
112 "E-CORE-NOTFOUND".to_string(),
113 "Resource not found".to_string(),
114 ),
115 Error::Gone => {
116 (StatusCode::GONE, "E-CORE-GONE".to_string(), "Resource is gone".to_string())
117 }
118 Error::PermissionDenied => (
119 StatusCode::FORBIDDEN,
120 "E-AUTH-NOPERM".to_string(),
121 "You do not have permission to access this resource".to_string(),
122 ),
123 Error::Unauthorized => (
124 StatusCode::UNAUTHORIZED,
125 "E-AUTH-UNAUTH".to_string(),
126 "Authentication required or invalid token".to_string(),
127 ),
128 Error::ValidationError(msg) => (
129 StatusCode::BAD_REQUEST,
130 "E-VAL-INVALID".to_string(),
131 format!("Request validation failed: {}", msg),
132 ),
133 Error::Conflict(msg) => (
134 StatusCode::CONFLICT,
135 "E-CORE-CONFLICT".to_string(),
136 format!("Resource conflict: {}", msg),
137 ),
138 Error::PreconditionRequired(msg) => (
139 StatusCode::PRECONDITION_REQUIRED,
140 "E-POW-REQUIRED".to_string(),
141 format!("Precondition required: {}", msg),
142 ),
143 Error::SettingNotFound(msg) => (
144 StatusCode::NOT_FOUND,
145 "E-SET-NOTFOUND".to_string(),
146 format!("Setting not configured: {}", msg),
147 ),
148 Error::Timeout => (
149 StatusCode::REQUEST_TIMEOUT,
150 "E-NET-TIMEOUT".to_string(),
151 "Request timeout".to_string(),
152 ),
153 Error::ServiceUnavailable(msg) => (
154 StatusCode::SERVICE_UNAVAILABLE,
155 "E-SYS-UNAVAIL".to_string(),
156 format!("Service temporarily unavailable: {}", msg),
157 ),
158 Error::DbError => (
160 StatusCode::INTERNAL_SERVER_ERROR,
161 "E-CORE-DBERR".to_string(),
162 "Internal server error".to_string(),
163 ),
164 Error::Internal(msg) => {
165 warn!("internal error: {}", msg);
166 (
167 StatusCode::INTERNAL_SERVER_ERROR,
168 "E-CORE-INTERNAL".to_string(),
169 "Internal server error".to_string(),
170 )
171 }
172 Error::Parse => (
173 StatusCode::INTERNAL_SERVER_ERROR,
174 "E-CORE-PARSE".to_string(),
175 "Internal server error".to_string(),
176 ),
177 Error::Io(_) => (
178 StatusCode::INTERNAL_SERVER_ERROR,
179 "E-SYS-IO".to_string(),
180 "Internal server error".to_string(),
181 ),
182 Error::NetworkError(_) => (
183 StatusCode::INTERNAL_SERVER_ERROR,
184 "E-NET-ERROR".to_string(),
185 "Internal server error".to_string(),
186 ),
187 Error::ImageError(_) => (
188 StatusCode::INTERNAL_SERVER_ERROR,
189 "E-IMG-PROCFAIL".to_string(),
190 "Internal server error".to_string(),
191 ),
192 Error::CryptoError(_) => (
193 StatusCode::INTERNAL_SERVER_ERROR,
194 "E-CRYPT-FAIL".to_string(),
195 "Internal server error".to_string(),
196 ),
197 Error::ConfigError(_) => (
198 StatusCode::INTERNAL_SERVER_ERROR,
199 "E-CONF-CFGERR".to_string(),
200 "Internal server error".to_string(),
201 ),
202 Error::FileSourceNotFound => (
203 StatusCode::NOT_FOUND,
204 "E-FILE-SRCNOTFOUND".to_string(),
205 "source_not_found".to_string(),
206 ),
207 Error::FileSourceForbidden => (
208 StatusCode::FORBIDDEN,
209 "E-FILE-SRCFORBID".to_string(),
210 "source_forbidden".to_string(),
211 ),
212 Error::FileSourceUnreachable => (
213 StatusCode::SERVICE_UNAVAILABLE,
214 "E-FILE-SRCUNREACH".to_string(),
215 "source_unreachable".to_string(),
216 ),
217 Error::FileCycleRejected => (
218 StatusCode::BAD_REQUEST,
219 "E-FILE-CYCLEREJ".to_string(),
220 "cycle_rejected".to_string(),
221 ),
222 };
223
224 let error_response = ErrorResponse::new(code, message);
225 let mut response = (status, Json(error_response)).into_response();
226 response.headers_mut().insert(
227 axum::http::header::CACHE_CONTROL,
228 axum::http::HeaderValue::from_static("no-store"),
229 );
230 response
231 .headers_mut()
232 .insert(axum::http::header::PRAGMA, axum::http::HeaderValue::from_static("no-cache"));
233 response
234 }
235}
236
237impl From<std::num::ParseIntError> for Error {
238 fn from(err: std::num::ParseIntError) -> Self {
239 warn!("parse int error: {}", err);
240 Error::Parse
241 }
242}
243
244impl From<std::time::SystemTimeError> for Error {
245 fn from(err: std::time::SystemTimeError) -> Self {
246 warn!("system time error: {}", err);
247 Error::ServiceUnavailable("system time error".into())
248 }
249}
250
251impl From<axum::Error> for Error {
252 fn from(err: axum::Error) -> Self {
253 warn!("axum error: {}", err);
254 Error::NetworkError("axum error".into())
255 }
256}
257
258impl From<axum::http::Error> for Error {
259 fn from(err: axum::http::Error) -> Self {
260 warn!("http error: {}", err);
261 Error::NetworkError("http error".into())
262 }
263}
264
265impl From<axum::http::header::ToStrError> for Error {
266 fn from(err: axum::http::header::ToStrError) -> Self {
267 warn!("header to str error: {}", err);
268 Error::Parse
269 }
270}
271
272impl From<serde_json::Error> for Error {
273 fn from(err: serde_json::Error) -> Self {
274 warn!("json error: {}", err);
275 Error::Parse
276 }
277}
278
279impl From<tokio::task::JoinError> for Error {
280 fn from(err: tokio::task::JoinError) -> Self {
281 warn!("tokio join error: {}", err);
282 Error::ServiceUnavailable("task execution failed".into())
283 }
284}
285
286#[cfg(feature = "server")]
289impl From<instant_acme::Error> for Error {
290 fn from(err: instant_acme::Error) -> Self {
291 warn!("acme error: {}", err);
292 Error::ConfigError("ACME certificate error".into())
293 }
294}
295
296#[cfg(feature = "server")]
297impl From<pem::PemError> for Error {
298 fn from(err: pem::PemError) -> Self {
299 warn!("pem error: {}", err);
300 Error::CryptoError("PEM parsing error".into())
301 }
302}
303
304#[cfg(feature = "server")]
305impl From<jsonwebtoken::errors::Error> for Error {
306 fn from(err: jsonwebtoken::errors::Error) -> Self {
307 warn!("jwt error: {}", err);
308 Error::Unauthorized
309 }
310}
311
312#[cfg(feature = "server")]
313impl From<x509_parser::asn1_rs::Err<x509_parser::error::X509Error>> for Error {
314 fn from(err: x509_parser::asn1_rs::Err<x509_parser::error::X509Error>) -> Self {
315 warn!("x509 error: {}", err);
316 Error::CryptoError("X.509 certificate error".into())
317 }
318}
319
320#[cfg(feature = "server")]
321impl From<rustls::Error> for Error {
322 fn from(err: rustls::Error) -> Self {
323 warn!("rustls error: {}", err);
324 Error::CryptoError("TLS error".into())
325 }
326}
327
328#[cfg(feature = "server")]
329impl From<rustls_pki_types::pem::Error> for Error {
330 fn from(err: rustls_pki_types::pem::Error) -> Self {
331 warn!("pem error: {}", err);
332 Error::CryptoError("PEM parsing error".into())
333 }
334}
335
336#[cfg(feature = "server")]
337impl From<hyper::Error> for Error {
338 fn from(err: hyper::Error) -> Self {
339 warn!("hyper error: {}", err);
340 Error::NetworkError("HTTP client error".into())
341 }
342}
343
344#[cfg(feature = "server")]
345impl From<hyper_util::client::legacy::Error> for Error {
346 fn from(err: hyper_util::client::legacy::Error) -> Self {
347 warn!("hyper error: {}", err);
348 Error::NetworkError("HTTP client error".into())
349 }
350}
351
352#[cfg(feature = "server")]
353impl From<image::error::ImageError> for Error {
354 fn from(err: image::error::ImageError) -> Self {
355 warn!("image error: {:?}", err);
356 Error::ImageError("Image processing failed".into())
357 }
358}
359
360#[macro_export]
384macro_rules! lock {
385 ($mutex:expr) => {
387 $mutex
388 .lock()
389 .map_err(|_| $crate::error::Error::Internal("mutex poisoned".into()))
390 };
391 ($mutex:expr, $context:expr) => {
393 $mutex
394 .lock()
395 .map_err(|_| $crate::error::Error::Internal(format!("mutex poisoned: {}", $context)))
396 };
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn permanent_errors_are_not_retryable() {
405 assert!(!Error::PermissionDenied.is_retryable());
406 assert!(!Error::Unauthorized.is_retryable());
407 assert!(!Error::ValidationError("x".into()).is_retryable());
408 assert!(!Error::Parse.is_retryable());
409 assert!(!Error::Gone.is_retryable());
410 }
411
412 #[test]
413 fn transient_errors_are_retryable() {
414 assert!(Error::Timeout.is_retryable());
415 assert!(Error::NetworkError("x".into()).is_retryable());
416 assert!(Error::DbError.is_retryable());
417 assert!(Error::NotFound.is_retryable()); assert!(Error::CryptoError("TLS error".into()).is_retryable());
420 }
421}
422
423