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<ParseError> for ReductError {
130    fn from(err: ParseError) -> Self {
131        // Keep the plain conversion for downstream APIs returning Result<_, ReductError>.
132        ReductError {
133            status: ErrorCode::UrlParseError,
134            message: err.to_string(),
135            source: None,
136        }
137    }
138}
139
140impl From<SystemTimeError> for ReductError {
141    fn from(err: SystemTimeError) -> Self {
142        // Keep the plain conversion for downstream APIs returning Result<_, ReductError>.
143        ReductError {
144            status: ErrorCode::InternalServerError,
145            message: err.to_string(),
146            source: None,
147        }
148    }
149}
150
151impl From<SystemTimeError> for ReductError<SystemTimeError> {
152    fn from(err: SystemTimeError) -> Self {
153        // A system time error is an internal reductstore error
154        ReductError {
155            status: ErrorCode::InternalServerError,
156            message: err.to_string(),
157            source: Some(err),
158        }
159    }
160}
161
162impl From<ParseError> for ReductError<ParseError> {
163    fn from(err: ParseError) -> Self {
164        // A parse error is an internal reductstore error
165        ReductError {
166            status: ErrorCode::UrlParseError,
167            message: err.to_string(),
168            source: Some(err),
169        }
170    }
171}
172
173impl<T> From<PoisonError<T>> for ReductError {
174    fn from(_: PoisonError<T>) -> Self {
175        // Keep the plain conversion for downstream APIs returning Result<_, ReductError>.
176        ReductError {
177            status: ErrorCode::InternalServerError,
178            message: "Poison error".to_string(),
179            source: None,
180        }
181    }
182}
183
184impl<T> From<PoisonError<T>> for ReductError<PoisonError<T>> {
185    fn from(err: PoisonError<T>) -> Self {
186        // A poison error is an internal reductstore error
187        ReductError {
188            status: ErrorCode::InternalServerError,
189            message: "Poison error".to_string(),
190            source: Some(err),
191        }
192    }
193}
194
195impl From<Box<dyn std::any::Any + Send>> for ReductError {
196    fn from(err: Box<dyn std::any::Any + Send>) -> Self {
197        // Keep the plain conversion for downstream APIs returning Result<_, ReductError>.
198        ReductError {
199            status: ErrorCode::InternalServerError,
200            message: format!("{:?}", err),
201            source: None,
202        }
203    }
204}
205
206#[cfg(feature = "io")]
207impl<T> From<SendError<T>> for ReductError {
208    fn from(err: SendError<T>) -> Self {
209        // Keep the plain conversion for downstream APIs returning Result<_, ReductError>.
210        ReductError {
211            status: ErrorCode::InternalServerError,
212            message: err.to_string(),
213            source: None,
214        }
215    }
216}
217
218#[cfg(feature = "io")]
219impl<T> From<SendError<T>> for ReductError<SendError<T>> {
220    fn from(err: SendError<T>) -> Self {
221        // A send error is an internal reductstore error
222        ReductError {
223            status: ErrorCode::InternalServerError,
224            message: err.to_string(),
225            source: Some(err),
226        }
227    }
228}
229
230impl Error for ReductError {
231    fn description(&self) -> &str {
232        &self.message
233    }
234}
235
236impl ReductError {
237    pub fn new(status: ErrorCode, message: &str) -> Self {
238        ReductError {
239            status,
240            message: message.to_string(),
241            source: None,
242        }
243    }
244
245    pub fn status(&self) -> ErrorCode {
246        self.status
247    }
248
249    pub fn message(&self) -> &str {
250        &self.message
251    }
252
253    pub fn ok() -> ReductError {
254        ReductError {
255            status: ErrorCode::OK,
256            message: "".to_string(),
257            source: None,
258        }
259    }
260
261    pub fn timeout(msg: &str) -> ReductError {
262        ReductError {
263            status: ErrorCode::Timeout,
264            message: msg.to_string(),
265            source: None,
266        }
267    }
268
269    /// Create a no content error.
270    pub fn no_content(msg: &str) -> ReductError {
271        ReductError {
272            status: ErrorCode::NoContent,
273            message: msg.to_string(),
274            source: None,
275        }
276    }
277
278    /// Create a not found error.
279    pub fn not_found(msg: &str) -> ReductError {
280        ReductError {
281            status: ErrorCode::NotFound,
282            message: msg.to_string(),
283            source: None,
284        }
285    }
286
287    /// Create a conflict error.
288    pub fn conflict(msg: &str) -> ReductError {
289        ReductError {
290            status: ErrorCode::Conflict,
291            message: msg.to_string(),
292            source: None,
293        }
294    }
295
296    /// Create a bad request error.
297    pub fn bad_request(msg: &str) -> ReductError {
298        ReductError {
299            status: ErrorCode::BadRequest,
300            message: msg.to_string(),
301            source: None,
302        }
303    }
304
305    /// Create an unauthorized error.
306    pub fn unauthorized(msg: &str) -> ReductError {
307        ReductError {
308            status: ErrorCode::Unauthorized,
309            message: msg.to_string(),
310            source: None,
311        }
312    }
313
314    /// Create a forbidden error.
315    pub fn forbidden(msg: &str) -> ReductError {
316        ReductError {
317            status: ErrorCode::Forbidden,
318            message: msg.to_string(),
319            source: None,
320        }
321    }
322
323    /// Create an unprocessable entity error.
324    pub fn unprocessable_entity(msg: &str) -> ReductError {
325        ReductError {
326            status: ErrorCode::UnprocessableEntity,
327            message: msg.to_string(),
328            source: None,
329        }
330    }
331
332    /// Create a too early error.
333    pub fn too_early(msg: &str) -> ReductError {
334        ReductError {
335            status: ErrorCode::TooEarly,
336            message: msg.to_string(),
337            source: None,
338        }
339    }
340
341    /// Create a bad request error.
342    pub fn internal_server_error(msg: &str) -> ReductError {
343        ReductError {
344            status: ErrorCode::InternalServerError,
345            message: msg.to_string(),
346            source: None,
347        }
348    }
349}
350
351// Macros for creating errors with a message.
352#[macro_export]
353macro_rules! ok {
354    () => {
355        ReductError::ok()
356    };
357}
358
359#[macro_export]
360macro_rules! timeout {
361    ($msg:expr, $($arg:tt)*) => {
362        ReductError::timeout(&format!($msg, $($arg)*))
363    };
364    ($msg:expr) => {
365        ReductError::timeout($msg)
366    };
367}
368
369#[macro_export]
370macro_rules! no_content {
371    ($msg:expr, $($arg:tt)*) => {
372        ReductError::no_content(&format!($msg, $($arg)*))
373    };
374    ($msg:expr) => {
375        ReductError::no_content($msg)
376    };
377}
378
379#[macro_export]
380macro_rules! bad_request {
381    ($msg:expr, $($arg:tt)*) => {
382        ReductError::bad_request(&format!($msg, $($arg)*))
383    };
384    ($msg:expr) => {
385        ReductError::bad_request($msg)
386    };
387}
388
389#[macro_export]
390macro_rules! forbidden {
391    ($msg:expr, $($arg:tt)*) => {
392        ReductError::forbidden(&format!($msg, $($arg)*))
393    };
394    ($msg:expr) => {
395        ReductError::forbidden($msg)
396    };
397}
398
399#[macro_export]
400macro_rules! unprocessable_entity {
401    ($msg:expr, $($arg:tt)*) => {
402        ReductError::unprocessable_entity(&format!($msg, $($arg)*))
403    };
404    ($msg:expr) => {
405        ReductError::unprocessable_entity($msg)
406    };
407}
408
409#[macro_export]
410macro_rules! not_found {
411    ($msg:expr, $($arg:tt)*) => {
412        ReductError::not_found(&format!($msg, $($arg)*))
413    };
414    ($msg:expr) => {
415        ReductError::not_found($msg)
416    };
417}
418#[macro_export]
419macro_rules! conflict {
420    ($msg:expr, $($arg:tt)*) => {
421        ReductError::conflict(&format!($msg, $($arg)*))
422    };
423    ($msg:expr) => {
424        ReductError::conflict($msg)
425    };
426}
427
428#[macro_export]
429macro_rules! too_early {
430    ($msg:expr, $($arg:tt)*) => {
431        ReductError::too_early(&format!($msg, $($arg)*))
432    };
433    ($msg:expr) => {
434        ReductError::too_early($msg)
435    };
436}
437
438#[macro_export]
439macro_rules! internal_server_error {
440    ($msg:expr, $($arg:tt)*) => {
441        ReductError::internal_server_error(&format!($msg, $($arg)*))
442    };
443    ($msg:expr) => {
444        ReductError::internal_server_error($msg)
445    };
446}
447
448#[macro_export]
449macro_rules! unauthorized {
450    ($msg:expr, $($arg:tt)*) => {
451        ReductError::unauthorized(&format!($msg, $($arg)*))
452    };
453    ($msg:expr) => {
454        ReductError::unauthorized($msg)
455    };
456}
457
458#[macro_export]
459macro_rules! service_unavailable {
460    ($msg:expr, $($arg:tt)*) => {
461        ReductError::new(ErrorCode::ServiceUnavailable, &format!($msg, $($arg)*))
462    };
463    ($msg:expr) => {
464        ReductError::new(ErrorCode::ServiceUnavailable, $msg)
465    };
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use std::fmt;
472    use std::time::{SystemTime, UNIX_EPOCH};
473
474    #[derive(Debug)]
475    struct CustomSource;
476
477    impl fmt::Display for CustomSource {
478        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479            write!(f, "custom source")
480        }
481    }
482
483    #[test]
484    fn creates_internal_server_error() {
485        let error = ReductError::internal_server_error("Unexpected server error");
486        assert_eq!(error.status, ErrorCode::InternalServerError);
487        assert_eq!(error.message, "Unexpected server error");
488    }
489
490    #[test]
491    fn converts_io_error_to_reduct_error() {
492        let io_error = std::io::Error::new(std::io::ErrorKind::Other, "IO failure");
493        let error: ReductError = io_error.into();
494        assert_eq!(error.status, ErrorCode::InternalServerError);
495        assert_eq!(error.message, "IO failure");
496    }
497
498    #[test]
499    fn converts_system_time_error_to_reduct_error() {
500        let system_time_error = UNIX_EPOCH.duration_since(SystemTime::now()).unwrap_err();
501        let error: ReductError = system_time_error.into();
502        assert_eq!(error.status, ErrorCode::InternalServerError);
503        assert_eq!(error.message, "second time provided was later than self");
504        assert_eq!(error.source, None);
505    }
506
507    #[test]
508    fn converts_system_time_error_to_typed_reduct_error() {
509        let system_time_error = UNIX_EPOCH.duration_since(SystemTime::now()).unwrap_err();
510        let error: ReductError<SystemTimeError> = system_time_error.into();
511        assert_eq!(error.status, ErrorCode::InternalServerError);
512        assert_eq!(error.message, "second time provided was later than self");
513        assert!(error.source.is_some());
514    }
515
516    #[test]
517    fn converts_url_parse_error_to_reduct_error() {
518        let parse_error = ParseError::EmptyHost;
519        let error: ReductError = parse_error.into();
520        assert_eq!(error.status, ErrorCode::UrlParseError);
521        assert_eq!(error.message, "empty host");
522        assert_eq!(error.source, None);
523    }
524
525    #[test]
526    fn converts_url_parse_error_to_typed_reduct_error() {
527        let parse_error = ParseError::EmptyHost;
528        let error: ReductError<ParseError> = parse_error.into();
529        assert_eq!(error.status, ErrorCode::UrlParseError);
530        assert_eq!(error.message, "empty host");
531        assert_eq!(error.source, Some(ParseError::EmptyHost));
532    }
533
534    #[test]
535    fn converts_poison_error_to_reduct_error() {
536        let poison_error: PoisonError<()> = PoisonError::new(());
537        let error: ReductError = poison_error.into();
538        assert_eq!(error.status, ErrorCode::InternalServerError);
539        assert_eq!(error.message, "Poison error");
540        assert_eq!(error.source, None);
541    }
542
543    #[test]
544    fn converts_poison_error_to_typed_reduct_error() {
545        let poison_error: PoisonError<()> = PoisonError::new(());
546        let error: ReductError<PoisonError<()>> = poison_error.into();
547        assert_eq!(error.status, ErrorCode::InternalServerError);
548        assert_eq!(error.message, "Poison error");
549        assert!(error.source.is_some());
550    }
551
552    #[test]
553    fn converts_boxed_any_to_reduct_error() {
554        let boxed_error: Box<dyn std::any::Any + Send> = Box::new("panic payload");
555        let error: ReductError = boxed_error.into();
556        assert_eq!(error.status, ErrorCode::InternalServerError);
557        assert_eq!(error.source, None);
558    }
559
560    #[test]
561    fn drops_source_from_typed_reduct_error() {
562        let error_with_source = ReductError {
563            status: ErrorCode::BadRequest,
564            message: "bad input".to_string(),
565            source: Some(CustomSource),
566        };
567
568        let error = error_with_source.without_source();
569        assert_eq!(error.status, ErrorCode::BadRequest);
570        assert_eq!(error.message, "bad input");
571        assert_eq!(error.source, None);
572    }
573
574    #[test]
575    fn converts_io_reduct_error_into_plain_error() {
576        let error_with_source = ReductError {
577            status: ErrorCode::InternalServerError,
578            message: "io typed error".to_string(),
579            source: Some(std::io::Error::other("boom")),
580        };
581
582        let error: ReductError = error_with_source.without_source();
583        assert_eq!(error.status, ErrorCode::InternalServerError);
584        assert_eq!(error.message, "io typed error");
585        assert_eq!(error.source, None);
586    }
587
588    #[cfg(feature = "io")]
589    #[test]
590    fn converts_send_error_to_reduct_error() {
591        let send_error: SendError<()> = SendError(());
592        let error: ReductError = send_error.into();
593        assert_eq!(error.status, ErrorCode::InternalServerError);
594        assert_eq!(error.message, "channel closed");
595        assert_eq!(error.source, None);
596    }
597
598    #[cfg(feature = "io")]
599    #[test]
600    fn converts_send_error_to_typed_reduct_error() {
601        let send_error: SendError<()> = SendError(());
602        let error: ReductError<SendError<()>> = send_error.into();
603        assert_eq!(error.status, ErrorCode::InternalServerError);
604        assert_eq!(error.message, "channel closed");
605        assert!(error.source.is_some());
606    }
607
608    mod macros {
609        use super::*;
610
611        #[test]
612        fn test_ok_macro() {
613            let error = ok!();
614            assert_eq!(error.status, ErrorCode::OK);
615            assert_eq!(error.message, "");
616        }
617
618        #[test]
619        fn test_timeout_macro() {
620            let error = timeout!("Timeout error: {}", 42);
621            assert_eq!(error.status, ErrorCode::Timeout);
622            assert_eq!(error.message, "Timeout error: 42");
623        }
624
625        #[test]
626        fn test_no_content_macro() {
627            let error = no_content!("No content error: {}", 42);
628            assert_eq!(error.status, ErrorCode::NoContent);
629            assert_eq!(error.message, "No content error: 42");
630        }
631
632        #[test]
633        fn test_bad_request_macro() {
634            let error = bad_request!("Bad request error: {}", 42);
635            assert_eq!(error.status, ErrorCode::BadRequest);
636            assert_eq!(error.message, "Bad request error: 42");
637        }
638
639        #[test]
640        fn test_unprocessable_entity_macro() {
641            let error = unprocessable_entity!("Unprocessable entity error: {}", 42);
642            assert_eq!(error.status, ErrorCode::UnprocessableEntity);
643            assert_eq!(error.message, "Unprocessable entity error: 42");
644        }
645
646        #[test]
647        fn test_not_found_macro() {
648            let error = not_found!("Not found error: {}", 42);
649            assert_eq!(error.status, ErrorCode::NotFound);
650            assert_eq!(error.message, "Not found error: 42");
651        }
652
653        #[test]
654        fn test_conflict_macro() {
655            let error = conflict!("Conflict error: {}", 42);
656            assert_eq!(error.status, ErrorCode::Conflict);
657            assert_eq!(error.message, "Conflict error: 42");
658        }
659
660        #[test]
661        fn test_too_early_macro() {
662            let error = too_early!("Too early error: {}", 42);
663            assert_eq!(error.status, ErrorCode::TooEarly);
664            assert_eq!(error.message, "Too early error: 42");
665        }
666
667        #[test]
668        fn test_internal_server_error_macro() {
669            let error = internal_server_error!("Internal server error: {}", 42);
670            assert_eq!(error.status, ErrorCode::InternalServerError);
671            assert_eq!(error.message, "Internal server error: 42");
672        }
673    }
674}