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