Skip to main content

cloudillo_types/
error.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Error handling subsystem. Implements a custom Error type.
5
6use axum::{http::StatusCode, response::IntoResponse, Json};
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	// Core errors
16	NotFound,
17	PermissionDenied,
18	Unauthorized, // 401 - missing/invalid auth token
19	DbError,
20	Parse,
21
22	// Input validation and constraints
23	ValidationError(String),      // 400 - invalid input data
24	Conflict(String),             // 409 - constraint violation (unique, foreign key, etc)
25	PreconditionRequired(String), // 428 - precondition required (e.g., PoW)
26
27	// Network and external services
28	NetworkError(String), // Network/federation failures
29	Timeout,              // Operation timeout
30
31	// System and configuration
32	ConfigError(String),        // Missing or invalid configuration
33	ServiceUnavailable(String), // 503 - temporary system failures
34	Internal(String),           // Internal invariant violations, for debugging
35
36	// Processing
37	ImageError(String),  // Image processing failures
38	CryptoError(String), // Cryptography/TLS configuration errors
39
40	// externals
41	Io(std::io::Error),
42}
43
44impl From<std::io::Error> for Error {
45	fn from(err: std::io::Error) -> Self {
46		warn!("io error: {}", err);
47		Self::Io(err)
48	}
49}
50
51impl std::fmt::Display for Error {
52	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
53		write!(f, "{:?}", self)
54	}
55}
56
57impl std::error::Error for Error {}
58
59impl IntoResponse for Error {
60	fn into_response(self) -> axum::response::Response {
61		let (status, code, message) = match self {
62			Error::NotFound => (
63				StatusCode::NOT_FOUND,
64				"E-CORE-NOTFOUND".to_string(),
65				"Resource not found".to_string(),
66			),
67			Error::PermissionDenied => (
68				StatusCode::FORBIDDEN,
69				"E-AUTH-NOPERM".to_string(),
70				"You do not have permission to access this resource".to_string(),
71			),
72			Error::Unauthorized => (
73				StatusCode::UNAUTHORIZED,
74				"E-AUTH-UNAUTH".to_string(),
75				"Authentication required or invalid token".to_string(),
76			),
77			Error::ValidationError(msg) => (
78				StatusCode::BAD_REQUEST,
79				"E-VAL-INVALID".to_string(),
80				format!("Request validation failed: {}", msg),
81			),
82			Error::Conflict(msg) => (
83				StatusCode::CONFLICT,
84				"E-CORE-CONFLICT".to_string(),
85				format!("Resource conflict: {}", msg),
86			),
87			Error::PreconditionRequired(msg) => (
88				StatusCode::PRECONDITION_REQUIRED,
89				"E-POW-REQUIRED".to_string(),
90				format!("Precondition required: {}", msg),
91			),
92			Error::Timeout => (
93				StatusCode::REQUEST_TIMEOUT,
94				"E-NET-TIMEOUT".to_string(),
95				"Request timeout".to_string(),
96			),
97			Error::ServiceUnavailable(msg) => (
98				StatusCode::SERVICE_UNAVAILABLE,
99				"E-SYS-UNAVAIL".to_string(),
100				format!("Service temporarily unavailable: {}", msg),
101			),
102			// Server errors (5xx) - no message exposure for security
103			Error::DbError => (
104				StatusCode::INTERNAL_SERVER_ERROR,
105				"E-CORE-DBERR".to_string(),
106				"Internal server error".to_string(),
107			),
108			Error::Internal(msg) => {
109				warn!("internal error: {}", msg);
110				(
111					StatusCode::INTERNAL_SERVER_ERROR,
112					"E-CORE-INTERNAL".to_string(),
113					"Internal server error".to_string(),
114				)
115			}
116			Error::Parse => (
117				StatusCode::INTERNAL_SERVER_ERROR,
118				"E-CORE-PARSE".to_string(),
119				"Internal server error".to_string(),
120			),
121			Error::Io(_) => (
122				StatusCode::INTERNAL_SERVER_ERROR,
123				"E-SYS-IO".to_string(),
124				"Internal server error".to_string(),
125			),
126			Error::NetworkError(_) => (
127				StatusCode::INTERNAL_SERVER_ERROR,
128				"E-NET-ERROR".to_string(),
129				"Internal server error".to_string(),
130			),
131			Error::ImageError(_) => (
132				StatusCode::INTERNAL_SERVER_ERROR,
133				"E-IMG-PROCFAIL".to_string(),
134				"Internal server error".to_string(),
135			),
136			Error::CryptoError(_) => (
137				StatusCode::INTERNAL_SERVER_ERROR,
138				"E-CRYPT-FAIL".to_string(),
139				"Internal server error".to_string(),
140			),
141			Error::ConfigError(_) => (
142				StatusCode::INTERNAL_SERVER_ERROR,
143				"E-CONF-CFGERR".to_string(),
144				"Internal server error".to_string(),
145			),
146		};
147
148		let error_response = ErrorResponse::new(code, message);
149		(status, Json(error_response)).into_response()
150	}
151}
152
153impl From<std::num::ParseIntError> for Error {
154	fn from(err: std::num::ParseIntError) -> Self {
155		warn!("parse int error: {}", err);
156		Error::Parse
157	}
158}
159
160impl From<std::time::SystemTimeError> for Error {
161	fn from(err: std::time::SystemTimeError) -> Self {
162		warn!("system time error: {}", err);
163		Error::ServiceUnavailable("system time error".into())
164	}
165}
166
167impl From<axum::Error> for Error {
168	fn from(err: axum::Error) -> Self {
169		warn!("axum error: {}", err);
170		Error::NetworkError("axum error".into())
171	}
172}
173
174impl From<axum::http::Error> for Error {
175	fn from(err: axum::http::Error) -> Self {
176		warn!("http error: {}", err);
177		Error::NetworkError("http error".into())
178	}
179}
180
181impl From<axum::http::header::ToStrError> for Error {
182	fn from(err: axum::http::header::ToStrError) -> Self {
183		warn!("header to str error: {}", err);
184		Error::Parse
185	}
186}
187
188impl From<serde_json::Error> for Error {
189	fn from(err: serde_json::Error) -> Self {
190		warn!("json error: {}", err);
191		Error::Parse
192	}
193}
194
195impl From<tokio::task::JoinError> for Error {
196	fn from(err: tokio::task::JoinError) -> Self {
197		warn!("tokio join error: {}", err);
198		Error::ServiceUnavailable("task execution failed".into())
199	}
200}
201
202// Server-specific From impls (behind "server" feature)
203
204#[cfg(feature = "server")]
205impl From<instant_acme::Error> for Error {
206	fn from(err: instant_acme::Error) -> Self {
207		warn!("acme error: {}", err);
208		Error::ConfigError("ACME certificate error".into())
209	}
210}
211
212#[cfg(feature = "server")]
213impl From<pem::PemError> for Error {
214	fn from(err: pem::PemError) -> Self {
215		warn!("pem error: {}", err);
216		Error::CryptoError("PEM parsing error".into())
217	}
218}
219
220#[cfg(feature = "server")]
221impl From<jsonwebtoken::errors::Error> for Error {
222	fn from(err: jsonwebtoken::errors::Error) -> Self {
223		warn!("jwt error: {}", err);
224		Error::Unauthorized
225	}
226}
227
228#[cfg(feature = "server")]
229impl From<x509_parser::asn1_rs::Err<x509_parser::error::X509Error>> for Error {
230	fn from(err: x509_parser::asn1_rs::Err<x509_parser::error::X509Error>) -> Self {
231		warn!("x509 error: {}", err);
232		Error::CryptoError("X.509 certificate error".into())
233	}
234}
235
236#[cfg(feature = "server")]
237impl From<rustls::Error> for Error {
238	fn from(err: rustls::Error) -> Self {
239		warn!("rustls error: {}", err);
240		Error::CryptoError("TLS error".into())
241	}
242}
243
244#[cfg(feature = "server")]
245impl From<rustls_pki_types::pem::Error> for Error {
246	fn from(err: rustls_pki_types::pem::Error) -> Self {
247		warn!("pem error: {}", err);
248		Error::CryptoError("PEM parsing error".into())
249	}
250}
251
252#[cfg(feature = "server")]
253impl From<hyper::Error> for Error {
254	fn from(err: hyper::Error) -> Self {
255		warn!("hyper error: {}", err);
256		Error::NetworkError("HTTP client error".into())
257	}
258}
259
260#[cfg(feature = "server")]
261impl From<hyper_util::client::legacy::Error> for Error {
262	fn from(err: hyper_util::client::legacy::Error) -> Self {
263		warn!("hyper error: {}", err);
264		Error::NetworkError("HTTP client error".into())
265	}
266}
267
268#[cfg(feature = "server")]
269impl From<image::error::ImageError> for Error {
270	fn from(err: image::error::ImageError) -> Self {
271		warn!("image error: {:?}", err);
272		Error::ImageError("Image processing failed".into())
273	}
274}
275
276/// Helper macro for locking mutexes with automatic internal error handling.
277///
278/// This macro simplifies the common pattern of locking a mutex and converting
279/// poisoning errors to `Error::Internal`. It automatically adds context about
280/// which mutex was poisoned.
281///
282/// # Examples
283///
284/// ```ignore
285/// // Without macro:
286/// let mut data = my_mutex.lock().map_err(|_| Error::Internal("mutex poisoned".into()))?;
287///
288/// // With macro:
289/// let mut data = lock!(my_mutex)?;
290/// ```
291///
292/// The macro also supports adding context information:
293///
294/// ```ignore
295/// // With context:
296/// let mut data = lock!(my_mutex, "task_queue")?;
297/// // Produces: Error::Internal("mutex poisoned: task_queue")
298/// ```
299#[macro_export]
300macro_rules! lock {
301	// Simple version without context
302	($mutex:expr) => {
303		$mutex
304			.lock()
305			.map_err(|_| $crate::error::Error::Internal("mutex poisoned".into()))
306	};
307	// Version with context description
308	($mutex:expr, $context:expr) => {
309		$mutex
310			.lock()
311			.map_err(|_| $crate::error::Error::Internal(format!("mutex poisoned: {}", $context)))
312	};
313}
314
315// vim: ts=4