Skip to main content

cloudillo_types/
error.rs

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