Skip to main content

reduct_base/
error.rs

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