reduct_base/
error.rs

1// Copyright 2023-2025 ReductSoftware UG
2// This Source Code Form is subject to the terms of the Mozilla Public
3//    License, v. 2.0. If a copy of the MPL was not distributed with this
4//    file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
6pub use int_enum::IntEnum;
7use std::error::Error;
8use std::fmt::{Debug, Display, Error as FmtError, Formatter};
9use std::sync::PoisonError;
10use std::time::SystemTimeError;
11use url::ParseError;
12
13#[cfg(feature = "io")]
14use tokio::sync::mpsc::error::SendError;
15
16/// HTTP status codes + client errors (negative).
17#[repr(i16)]
18#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, IntEnum)]
19pub enum ErrorCode {
20    InvalidRequest = -6, // used for invalid requests
21    Interrupt = -5,      // used for interrupting a long-running task or query
22    UrlParseError = -4,
23    ConnectionError = -3,
24    Timeout = -2,
25    Unknown = -1,
26
27    Continue = 100,
28    OK = 200,
29    Created = 201,
30    Accepted = 202,
31    NoContent = 204,
32    BadRequest = 400,
33    Unauthorized = 401,
34    Forbidden = 403,
35    NotFound = 404,
36    MethodNotAllowed = 405,
37    NotAcceptable = 406,
38    RequestTimeout = 408,
39    Conflict = 409,
40    Gone = 410,
41    LengthRequired = 411,
42    PreconditionFailed = 412,
43    PayloadTooLarge = 413,
44    URITooLong = 414,
45    UnsupportedMediaType = 415,
46    RangeNotSatisfiable = 416,
47    ExpectationFailed = 417,
48    ImATeapot = 418,
49    MisdirectedRequest = 421,
50    UnprocessableEntity = 422,
51    Locked = 423,
52    FailedDependency = 424,
53    TooEarly = 425,
54    UpgradeRequired = 426,
55    PreconditionRequired = 428,
56    TooManyRequests = 429,
57    RequestHeaderFieldsTooLarge = 431,
58    UnavailableForLegalReasons = 451,
59    InternalServerError = 500,
60    NotImplemented = 501,
61    BadGateway = 502,
62    ServiceUnavailable = 503,
63    GatewayTimeout = 504,
64    HTTPVersionNotSupported = 505,
65    VariantAlsoNegotiates = 506,
66    InsufficientStorage = 507,
67    LoopDetected = 508,
68    NotExtended = 510,
69    NetworkAuthenticationRequired = 511,
70}
71
72/// An HTTP error, we use it for error handling.
73#[derive(PartialEq, Debug, Clone)]
74pub struct ReductError {
75    /// The HTTP status code.
76    pub status: ErrorCode,
77
78    /// The human readable message.
79    pub message: String,
80}
81
82impl Display for ReductError {
83    fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
84        write!(f, "[{:?}] {}", self.status, self.message)
85    }
86}
87
88impl Display for ErrorCode {
89    fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
90        write!(f, "{}", self.int_value())
91    }
92}
93
94impl From<std::io::Error> for ReductError {
95    fn from(err: std::io::Error) -> Self {
96        // An IO error is an internal reductstore error
97        ReductError {
98            status: ErrorCode::InternalServerError,
99            message: err.to_string(),
100        }
101    }
102}
103
104impl From<SystemTimeError> for ReductError {
105    fn from(err: SystemTimeError) -> Self {
106        // A system time error is an internal reductstore error
107        ReductError {
108            status: ErrorCode::InternalServerError,
109            message: err.to_string(),
110        }
111    }
112}
113
114impl From<ParseError> for ReductError {
115    fn from(err: ParseError) -> Self {
116        // A parse error is an internal reductstore error
117        ReductError {
118            status: ErrorCode::UrlParseError,
119            message: err.to_string(),
120        }
121    }
122}
123
124impl<T> From<PoisonError<T>> for ReductError {
125    fn from(_: PoisonError<T>) -> Self {
126        // A poison error is an internal reductstore error
127        ReductError {
128            status: ErrorCode::InternalServerError,
129            message: "Poison error".to_string(),
130        }
131    }
132}
133
134impl From<Box<dyn std::any::Any + Send>> for ReductError {
135    fn from(err: Box<dyn std::any::Any + Send>) -> Self {
136        // A box error is an internal reductstore error
137        ReductError {
138            status: ErrorCode::InternalServerError,
139            message: format!("{:?}", err),
140        }
141    }
142}
143
144#[cfg(feature = "io")]
145impl<T> From<SendError<T>> for ReductError {
146    fn from(err: SendError<T>) -> Self {
147        // A send error is an internal reductstore error
148        ReductError {
149            status: ErrorCode::InternalServerError,
150            message: err.to_string(),
151        }
152    }
153}
154
155impl Error for ReductError {
156    fn description(&self) -> &str {
157        &self.message
158    }
159}
160
161impl ReductError {
162    pub fn new(status: ErrorCode, message: &str) -> Self {
163        ReductError {
164            status,
165            message: message.to_string(),
166        }
167    }
168
169    pub fn status(&self) -> ErrorCode {
170        self.status
171    }
172
173    pub fn message(&self) -> &str {
174        &self.message
175    }
176
177    pub fn ok() -> ReductError {
178        ReductError {
179            status: ErrorCode::OK,
180            message: "".to_string(),
181        }
182    }
183
184    pub fn timeout(msg: &str) -> ReductError {
185        ReductError {
186            status: ErrorCode::Timeout,
187            message: msg.to_string(),
188        }
189    }
190
191    /// Create a no content error.
192    pub fn no_content(msg: &str) -> ReductError {
193        ReductError {
194            status: ErrorCode::NoContent,
195            message: msg.to_string(),
196        }
197    }
198
199    /// Create a not found error.
200    pub fn not_found(msg: &str) -> ReductError {
201        ReductError {
202            status: ErrorCode::NotFound,
203            message: msg.to_string(),
204        }
205    }
206
207    /// Create a conflict error.
208    pub fn conflict(msg: &str) -> ReductError {
209        ReductError {
210            status: ErrorCode::Conflict,
211            message: msg.to_string(),
212        }
213    }
214
215    /// Create a bad request error.
216    pub fn bad_request(msg: &str) -> ReductError {
217        ReductError {
218            status: ErrorCode::BadRequest,
219            message: msg.to_string(),
220        }
221    }
222
223    /// Create an unauthorized error.
224    pub fn unauthorized(msg: &str) -> ReductError {
225        ReductError {
226            status: ErrorCode::Unauthorized,
227            message: msg.to_string(),
228        }
229    }
230
231    /// Create a forbidden error.
232    pub fn forbidden(msg: &str) -> ReductError {
233        ReductError {
234            status: ErrorCode::Forbidden,
235            message: msg.to_string(),
236        }
237    }
238
239    /// Create an unprocessable entity error.
240    pub fn unprocessable_entity(msg: &str) -> ReductError {
241        ReductError {
242            status: ErrorCode::UnprocessableEntity,
243            message: msg.to_string(),
244        }
245    }
246
247    /// Create a too early error.
248    pub fn too_early(msg: &str) -> ReductError {
249        ReductError {
250            status: ErrorCode::TooEarly,
251            message: msg.to_string(),
252        }
253    }
254
255    /// Create a bad request error.
256    pub fn internal_server_error(msg: &str) -> ReductError {
257        ReductError {
258            status: ErrorCode::InternalServerError,
259            message: msg.to_string(),
260        }
261    }
262}
263
264// Macros for creating errors with a message.
265
266#[macro_export]
267macro_rules! timeout {
268    ($msg:expr, $($arg:tt)*) => {
269        ReductError::timeout(&format!($msg, $($arg)*))
270    };
271    ($msg:expr) => {
272        ReductError::timeout($msg)
273    };
274}
275
276#[macro_export]
277macro_rules! no_content {
278    ($msg:expr, $($arg:tt)*) => {
279        ReductError::no_content(&format!($msg, $($arg)*))
280    };
281    ($msg:expr) => {
282        ReductError::no_content($msg)
283    };
284}
285
286#[macro_export]
287macro_rules! bad_request {
288    ($msg:expr, $($arg:tt)*) => {
289        ReductError::bad_request(&format!($msg, $($arg)*))
290    };
291    ($msg:expr) => {
292        ReductError::bad_request($msg)
293    };
294}
295
296#[macro_export]
297macro_rules! forbidden {
298    ($msg:expr, $($arg:tt)*) => {
299        ReductError::forbidden(&format!($msg, $($arg)*))
300    };
301    ($msg:expr) => {
302        ReductError::forbidden($msg)
303    };
304}
305
306#[macro_export]
307macro_rules! unprocessable_entity {
308    ($msg:expr, $($arg:tt)*) => {
309        ReductError::unprocessable_entity(&format!($msg, $($arg)*))
310    };
311    ($msg:expr) => {
312        ReductError::unprocessable_entity($msg)
313    };
314}
315
316#[macro_export]
317macro_rules! not_found {
318    ($msg:expr, $($arg:tt)*) => {
319        ReductError::not_found(&format!($msg, $($arg)*))
320    };
321    ($msg:expr) => {
322        ReductError::not_found($msg)
323    };
324}
325#[macro_export]
326macro_rules! conflict {
327    ($msg:expr, $($arg:tt)*) => {
328        ReductError::conflict(&format!($msg, $($arg)*))
329    };
330    ($msg:expr) => {
331        ReductError::conflict($msg)
332    };
333}
334
335#[macro_export]
336macro_rules! too_early {
337    ($msg:expr, $($arg:tt)*) => {
338        ReductError::too_early(&format!($msg, $($arg)*))
339    };
340    ($msg:expr) => {
341        ReductError::too_early($msg)
342    };
343}
344
345#[macro_export]
346macro_rules! internal_server_error {
347    ($msg:expr, $($arg:tt)*) => {
348        ReductError::internal_server_error(&format!($msg, $($arg)*))
349    };
350    ($msg:expr) => {
351        ReductError::internal_server_error($msg)
352    };
353}
354
355#[macro_export]
356macro_rules! unauthorized {
357    ($msg:expr, $($arg:tt)*) => {
358        ReductError::unauthorized(&format!($msg, $($arg)*))
359    };
360    ($msg:expr) => {
361        ReductError::unauthorized($msg)
362    };
363}
364
365#[macro_export]
366macro_rules! service_unavailable {
367    ($msg:expr, $($arg:tt)*) => {
368        ReductError::new(ErrorCode::ServiceUnavailable, &format!($msg, $($arg)*))
369    };
370    ($msg:expr) => {
371        ReductError::new(ErrorCode::ServiceUnavailable, $msg)
372    };
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use std::time::{SystemTime, UNIX_EPOCH};
379
380    #[test]
381    fn creates_internal_server_error() {
382        let error = ReductError::internal_server_error("Unexpected server error");
383        assert_eq!(error.status, ErrorCode::InternalServerError);
384        assert_eq!(error.message, "Unexpected server error");
385    }
386
387    #[test]
388    fn converts_io_error_to_reduct_error() {
389        let io_error = std::io::Error::new(std::io::ErrorKind::Other, "IO failure");
390        let error: ReductError = io_error.into();
391        assert_eq!(error.status, ErrorCode::InternalServerError);
392        assert_eq!(error.message, "IO failure");
393    }
394
395    #[test]
396    fn converts_system_time_error_to_reduct_error() {
397        let system_time_error = UNIX_EPOCH.duration_since(SystemTime::now()).unwrap_err();
398        let error: ReductError = system_time_error.into();
399        assert_eq!(error.status, ErrorCode::InternalServerError);
400        assert_eq!(error.message, "second time provided was later than self");
401    }
402
403    #[test]
404    fn converts_url_parse_error_to_reduct_error() {
405        let parse_error = ParseError::EmptyHost;
406        let error: ReductError = parse_error.into();
407        assert_eq!(error.status, ErrorCode::UrlParseError);
408        assert_eq!(error.message, "empty host");
409    }
410
411    #[test]
412    fn converts_poison_error_to_reduct_error() {
413        let poison_error: PoisonError<()> = PoisonError::new(());
414        let error: ReductError = poison_error.into();
415        assert_eq!(error.status, ErrorCode::InternalServerError);
416        assert_eq!(error.message, "Poison error");
417    }
418
419    #[cfg(feature = "io")]
420    #[test]
421    fn converts_send_error_to_reduct_error() {
422        let send_error: SendError<()> = SendError(());
423        let error: ReductError = send_error.into();
424        assert_eq!(error.status, ErrorCode::InternalServerError);
425        assert_eq!(error.message, "channel closed");
426    }
427
428    mod macros {
429        use super::*;
430
431        #[test]
432        fn test_timeout_macro() {
433            let error = timeout!("Timeout error: {}", 42);
434            assert_eq!(error.status, ErrorCode::Timeout);
435            assert_eq!(error.message, "Timeout error: 42");
436        }
437
438        #[test]
439        fn test_no_content_macro() {
440            let error = no_content!("No content error: {}", 42);
441            assert_eq!(error.status, ErrorCode::NoContent);
442            assert_eq!(error.message, "No content error: 42");
443        }
444
445        #[test]
446        fn test_bad_request_macro() {
447            let error = bad_request!("Bad request error: {}", 42);
448            assert_eq!(error.status, ErrorCode::BadRequest);
449            assert_eq!(error.message, "Bad request error: 42");
450        }
451
452        #[test]
453        fn test_unprocessable_entity_macro() {
454            let error = unprocessable_entity!("Unprocessable entity error: {}", 42);
455            assert_eq!(error.status, ErrorCode::UnprocessableEntity);
456            assert_eq!(error.message, "Unprocessable entity error: 42");
457        }
458
459        #[test]
460        fn test_not_found_macro() {
461            let error = not_found!("Not found error: {}", 42);
462            assert_eq!(error.status, ErrorCode::NotFound);
463            assert_eq!(error.message, "Not found error: 42");
464        }
465
466        #[test]
467        fn test_conflict_macro() {
468            let error = conflict!("Conflict error: {}", 42);
469            assert_eq!(error.status, ErrorCode::Conflict);
470            assert_eq!(error.message, "Conflict error: 42");
471        }
472
473        #[test]
474        fn test_too_early_macro() {
475            let error = too_early!("Too early error: {}", 42);
476            assert_eq!(error.status, ErrorCode::TooEarly);
477            assert_eq!(error.message, "Too early error: 42");
478        }
479
480        #[test]
481        fn test_internal_server_error_macro() {
482            let error = internal_server_error!("Internal server error: {}", 42);
483            assert_eq!(error.status, ErrorCode::InternalServerError);
484            assert_eq!(error.message, "Internal server error: 42");
485        }
486    }
487}