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<Original = ()> {
73    /// The HTTP status code.
74    pub status: ErrorCode,
75
76    /// The human-readable message.
77    pub message: String,
78
79    /// The original err
80    pub source: Option<Original>,
81}
82
83impl<Original> ReductError<Original> {
84    /// Convert to a plain `ReductError` by dropping the original source type.
85    pub fn without_source(self) -> ReductError {
86        ReductError {
87            status: self.status,
88            message: self.message,
89            source: None,
90        }
91    }
92
93    pub fn with_source<T>(self, source: T) -> ReductError<T> {
94        ReductError {
95            status: self.status,
96            message: self.message,
97            source: Some(source),
98        }
99    }
100}
101
102impl<Original> Display for ReductError<Original> {
103    fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
104        write!(f, "[{:?}] {}", self.status, self.message)
105    }
106}
107
108impl Display for ErrorCode {
109    fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
110        write!(f, "{}", i16::from(*self))
111    }
112}
113
114impl From<std::io::Error> for ReductError {
115    fn from(err: std::io::Error) -> Self {
116        let status = match err.kind() {
117            std::io::ErrorKind::Unsupported => ErrorCode::MethodNotAllowed,
118            _ => ErrorCode::InternalServerError,
119        };
120
121        ReductError {
122            status,
123            message: err.to_string(),
124            source: None,
125        }
126    }
127}
128
129impl From<SystemTimeError> for ReductError<SystemTimeError> {
130    fn from(err: SystemTimeError) -> Self {
131        // A system time error is an internal reductstore error
132        ReductError {
133            status: ErrorCode::InternalServerError,
134            message: err.to_string(),
135            source: Some(err),
136        }
137    }
138}
139
140impl From<ParseError> for ReductError<ParseError> {
141    fn from(err: ParseError) -> Self {
142        // A parse error is an internal reductstore error
143        ReductError {
144            status: ErrorCode::UrlParseError,
145            message: err.to_string(),
146            source: Some(err),
147        }
148    }
149}
150
151impl<T> From<PoisonError<T>> for ReductError<PoisonError<T>> {
152    fn from(err: PoisonError<T>) -> Self {
153        // A poison error is an internal reductstore error
154        ReductError {
155            status: ErrorCode::InternalServerError,
156            message: "Poison error".to_string(),
157            source: Some(err),
158        }
159    }
160}
161
162#[cfg(feature = "io")]
163impl<T> From<SendError<T>> for ReductError<SendError<T>> {
164    fn from(err: SendError<T>) -> Self {
165        // A send error is an internal reductstore error
166        ReductError {
167            status: ErrorCode::InternalServerError,
168            message: err.to_string(),
169            source: Some(err),
170        }
171    }
172}
173
174impl Error for ReductError {
175    fn description(&self) -> &str {
176        &self.message
177    }
178}
179
180impl ReductError {
181    pub fn new(status: ErrorCode, message: &str) -> Self {
182        ReductError {
183            status,
184            message: message.to_string(),
185            source: None,
186        }
187    }
188
189    pub fn status(&self) -> ErrorCode {
190        self.status
191    }
192
193    pub fn message(&self) -> &str {
194        &self.message
195    }
196
197    pub fn ok() -> ReductError {
198        ReductError {
199            status: ErrorCode::OK,
200            message: "".to_string(),
201            source: None,
202        }
203    }
204
205    pub fn timeout(msg: &str) -> ReductError {
206        ReductError {
207            status: ErrorCode::Timeout,
208            message: msg.to_string(),
209            source: None,
210        }
211    }
212
213    /// Create a no content error.
214    pub fn no_content(msg: &str) -> ReductError {
215        ReductError {
216            status: ErrorCode::NoContent,
217            message: msg.to_string(),
218            source: None,
219        }
220    }
221
222    /// Create a not found error.
223    pub fn not_found(msg: &str) -> ReductError {
224        ReductError {
225            status: ErrorCode::NotFound,
226            message: msg.to_string(),
227            source: None,
228        }
229    }
230
231    /// Create a conflict error.
232    pub fn conflict(msg: &str) -> ReductError {
233        ReductError {
234            status: ErrorCode::Conflict,
235            message: msg.to_string(),
236            source: None,
237        }
238    }
239
240    /// Create a bad request error.
241    pub fn bad_request(msg: &str) -> ReductError {
242        ReductError {
243            status: ErrorCode::BadRequest,
244            message: msg.to_string(),
245            source: None,
246        }
247    }
248
249    /// Create an unauthorized error.
250    pub fn unauthorized(msg: &str) -> ReductError {
251        ReductError {
252            status: ErrorCode::Unauthorized,
253            message: msg.to_string(),
254            source: None,
255        }
256    }
257
258    /// Create a forbidden error.
259    pub fn forbidden(msg: &str) -> ReductError {
260        ReductError {
261            status: ErrorCode::Forbidden,
262            message: msg.to_string(),
263            source: None,
264        }
265    }
266
267    /// Create an unprocessable entity error.
268    pub fn unprocessable_entity(msg: &str) -> ReductError {
269        ReductError {
270            status: ErrorCode::UnprocessableEntity,
271            message: msg.to_string(),
272            source: None,
273        }
274    }
275
276    /// Create a too early error.
277    pub fn too_early(msg: &str) -> ReductError {
278        ReductError {
279            status: ErrorCode::TooEarly,
280            message: msg.to_string(),
281            source: None,
282        }
283    }
284
285    /// Create a bad request error.
286    pub fn internal_server_error(msg: &str) -> ReductError {
287        ReductError {
288            status: ErrorCode::InternalServerError,
289            message: msg.to_string(),
290            source: None,
291        }
292    }
293}
294
295// Macros for creating errors with a message.
296#[macro_export]
297macro_rules! ok {
298    () => {
299        ReductError::ok()
300    };
301}
302
303#[macro_export]
304macro_rules! timeout {
305    ($msg:expr, $($arg:tt)*) => {
306        ReductError::timeout(&format!($msg, $($arg)*))
307    };
308    ($msg:expr) => {
309        ReductError::timeout($msg)
310    };
311}
312
313#[macro_export]
314macro_rules! no_content {
315    ($msg:expr, $($arg:tt)*) => {
316        ReductError::no_content(&format!($msg, $($arg)*))
317    };
318    ($msg:expr) => {
319        ReductError::no_content($msg)
320    };
321}
322
323#[macro_export]
324macro_rules! bad_request {
325    ($msg:expr, $($arg:tt)*) => {
326        ReductError::bad_request(&format!($msg, $($arg)*))
327    };
328    ($msg:expr) => {
329        ReductError::bad_request($msg)
330    };
331}
332
333#[macro_export]
334macro_rules! forbidden {
335    ($msg:expr, $($arg:tt)*) => {
336        ReductError::forbidden(&format!($msg, $($arg)*))
337    };
338    ($msg:expr) => {
339        ReductError::forbidden($msg)
340    };
341}
342
343#[macro_export]
344macro_rules! unprocessable_entity {
345    ($msg:expr, $($arg:tt)*) => {
346        ReductError::unprocessable_entity(&format!($msg, $($arg)*))
347    };
348    ($msg:expr) => {
349        ReductError::unprocessable_entity($msg)
350    };
351}
352
353#[macro_export]
354macro_rules! not_found {
355    ($msg:expr, $($arg:tt)*) => {
356        ReductError::not_found(&format!($msg, $($arg)*))
357    };
358    ($msg:expr) => {
359        ReductError::not_found($msg)
360    };
361}
362#[macro_export]
363macro_rules! conflict {
364    ($msg:expr, $($arg:tt)*) => {
365        ReductError::conflict(&format!($msg, $($arg)*))
366    };
367    ($msg:expr) => {
368        ReductError::conflict($msg)
369    };
370}
371
372#[macro_export]
373macro_rules! too_early {
374    ($msg:expr, $($arg:tt)*) => {
375        ReductError::too_early(&format!($msg, $($arg)*))
376    };
377    ($msg:expr) => {
378        ReductError::too_early($msg)
379    };
380}
381
382#[macro_export]
383macro_rules! internal_server_error {
384    ($msg:expr, $($arg:tt)*) => {
385        ReductError::internal_server_error(&format!($msg, $($arg)*))
386    };
387    ($msg:expr) => {
388        ReductError::internal_server_error($msg)
389    };
390}
391
392#[macro_export]
393macro_rules! unauthorized {
394    ($msg:expr, $($arg:tt)*) => {
395        ReductError::unauthorized(&format!($msg, $($arg)*))
396    };
397    ($msg:expr) => {
398        ReductError::unauthorized($msg)
399    };
400}
401
402#[macro_export]
403macro_rules! service_unavailable {
404    ($msg:expr, $($arg:tt)*) => {
405        ReductError::new(ErrorCode::ServiceUnavailable, &format!($msg, $($arg)*))
406    };
407    ($msg:expr) => {
408        ReductError::new(ErrorCode::ServiceUnavailable, $msg)
409    };
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use std::fmt;
416    use std::time::{SystemTime, UNIX_EPOCH};
417
418    #[derive(Debug)]
419    struct CustomSource;
420
421    impl fmt::Display for CustomSource {
422        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423            write!(f, "custom source")
424        }
425    }
426
427    #[test]
428    fn creates_internal_server_error() {
429        let error = ReductError::internal_server_error("Unexpected server error");
430        assert_eq!(error.status, ErrorCode::InternalServerError);
431        assert_eq!(error.message, "Unexpected server error");
432    }
433
434    #[test]
435    fn converts_io_error_to_reduct_error() {
436        let io_error = std::io::Error::new(std::io::ErrorKind::Other, "IO failure");
437        let error: ReductError = io_error.into();
438        assert_eq!(error.status, ErrorCode::InternalServerError);
439        assert_eq!(error.message, "IO failure");
440    }
441
442    #[test]
443    fn converts_system_time_error_to_reduct_error() {
444        let system_time_error = UNIX_EPOCH.duration_since(SystemTime::now()).unwrap_err();
445        let error: ReductError<_> = system_time_error.into();
446        assert_eq!(error.status, ErrorCode::InternalServerError);
447        assert_eq!(error.message, "second time provided was later than self");
448    }
449
450    #[test]
451    fn converts_url_parse_error_to_reduct_error() {
452        let parse_error = ParseError::EmptyHost;
453        let error: ReductError<_> = parse_error.into();
454        assert_eq!(error.status, ErrorCode::UrlParseError);
455        assert_eq!(error.message, "empty host");
456    }
457
458    #[test]
459    fn converts_poison_error_to_reduct_error() {
460        let poison_error: PoisonError<()> = PoisonError::new(());
461        let error: ReductError<_> = poison_error.into();
462        assert_eq!(error.status, ErrorCode::InternalServerError);
463        assert_eq!(error.message, "Poison error");
464    }
465
466    #[test]
467    fn drops_source_from_typed_reduct_error() {
468        let error_with_source = ReductError {
469            status: ErrorCode::BadRequest,
470            message: "bad input".to_string(),
471            source: Some(CustomSource),
472        };
473
474        let error = error_with_source.without_source();
475        assert_eq!(error.status, ErrorCode::BadRequest);
476        assert_eq!(error.message, "bad input");
477        assert_eq!(error.source, None);
478    }
479
480    #[test]
481    fn converts_io_reduct_error_into_plain_error() {
482        let error_with_source = ReductError {
483            status: ErrorCode::InternalServerError,
484            message: "io typed error".to_string(),
485            source: Some(std::io::Error::other("boom")),
486        };
487
488        let error: ReductError = error_with_source.without_source();
489        assert_eq!(error.status, ErrorCode::InternalServerError);
490        assert_eq!(error.message, "io typed error");
491        assert_eq!(error.source, None);
492    }
493
494    #[cfg(feature = "io")]
495    #[test]
496    fn converts_send_error_to_reduct_error() {
497        let send_error: SendError<()> = SendError(());
498        let error: ReductError<_> = send_error.into();
499        assert_eq!(error.status, ErrorCode::InternalServerError);
500        assert_eq!(error.message, "channel closed");
501    }
502
503    mod macros {
504        use super::*;
505
506        #[test]
507        fn test_ok_macro() {
508            let error = ok!();
509            assert_eq!(error.status, ErrorCode::OK);
510            assert_eq!(error.message, "");
511        }
512
513        #[test]
514        fn test_timeout_macro() {
515            let error = timeout!("Timeout error: {}", 42);
516            assert_eq!(error.status, ErrorCode::Timeout);
517            assert_eq!(error.message, "Timeout error: 42");
518        }
519
520        #[test]
521        fn test_no_content_macro() {
522            let error = no_content!("No content error: {}", 42);
523            assert_eq!(error.status, ErrorCode::NoContent);
524            assert_eq!(error.message, "No content error: 42");
525        }
526
527        #[test]
528        fn test_bad_request_macro() {
529            let error = bad_request!("Bad request error: {}", 42);
530            assert_eq!(error.status, ErrorCode::BadRequest);
531            assert_eq!(error.message, "Bad request error: 42");
532        }
533
534        #[test]
535        fn test_unprocessable_entity_macro() {
536            let error = unprocessable_entity!("Unprocessable entity error: {}", 42);
537            assert_eq!(error.status, ErrorCode::UnprocessableEntity);
538            assert_eq!(error.message, "Unprocessable entity error: 42");
539        }
540
541        #[test]
542        fn test_not_found_macro() {
543            let error = not_found!("Not found error: {}", 42);
544            assert_eq!(error.status, ErrorCode::NotFound);
545            assert_eq!(error.message, "Not found error: 42");
546        }
547
548        #[test]
549        fn test_conflict_macro() {
550            let error = conflict!("Conflict error: {}", 42);
551            assert_eq!(error.status, ErrorCode::Conflict);
552            assert_eq!(error.message, "Conflict error: 42");
553        }
554
555        #[test]
556        fn test_too_early_macro() {
557            let error = too_early!("Too early error: {}", 42);
558            assert_eq!(error.status, ErrorCode::TooEarly);
559            assert_eq!(error.message, "Too early error: 42");
560        }
561
562        #[test]
563        fn test_internal_server_error_macro() {
564            let error = internal_server_error!("Internal server error: {}", 42);
565            assert_eq!(error.status, ErrorCode::InternalServerError);
566            assert_eq!(error.message, "Internal server error: 42");
567        }
568    }
569}