Skip to main content

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, "{}", i16::from(*self))
91    }
92}
93
94impl From<std::io::Error> for ReductError {
95    fn from(err: std::io::Error) -> Self {
96        let status = match err.kind() {
97            std::io::ErrorKind::Unsupported => ErrorCode::MethodNotAllowed,
98            _ => ErrorCode::InternalServerError,
99        };
100
101        ReductError {
102            status,
103            message: err.to_string(),
104        }
105    }
106}
107
108impl From<SystemTimeError> for ReductError {
109    fn from(err: SystemTimeError) -> Self {
110        // A system time error is an internal reductstore error
111        ReductError {
112            status: ErrorCode::InternalServerError,
113            message: err.to_string(),
114        }
115    }
116}
117
118impl From<ParseError> for ReductError {
119    fn from(err: ParseError) -> Self {
120        // A parse error is an internal reductstore error
121        ReductError {
122            status: ErrorCode::UrlParseError,
123            message: err.to_string(),
124        }
125    }
126}
127
128impl<T> From<PoisonError<T>> for ReductError {
129    fn from(_: PoisonError<T>) -> Self {
130        // A poison error is an internal reductstore error
131        ReductError {
132            status: ErrorCode::InternalServerError,
133            message: "Poison error".to_string(),
134        }
135    }
136}
137
138impl From<Box<dyn std::any::Any + Send>> for ReductError {
139    fn from(err: Box<dyn std::any::Any + Send>) -> Self {
140        // A box error is an internal reductstore error
141        ReductError {
142            status: ErrorCode::InternalServerError,
143            message: format!("{:?}", err),
144        }
145    }
146}
147
148#[cfg(feature = "io")]
149impl<T> From<SendError<T>> for ReductError {
150    fn from(err: SendError<T>) -> Self {
151        // A send error is an internal reductstore error
152        ReductError {
153            status: ErrorCode::InternalServerError,
154            message: err.to_string(),
155        }
156    }
157}
158
159impl Error for ReductError {
160    fn description(&self) -> &str {
161        &self.message
162    }
163}
164
165impl ReductError {
166    pub fn new(status: ErrorCode, message: &str) -> Self {
167        ReductError {
168            status,
169            message: message.to_string(),
170        }
171    }
172
173    pub fn status(&self) -> ErrorCode {
174        self.status
175    }
176
177    pub fn message(&self) -> &str {
178        &self.message
179    }
180
181    pub fn ok() -> ReductError {
182        ReductError {
183            status: ErrorCode::OK,
184            message: "".to_string(),
185        }
186    }
187
188    pub fn timeout(msg: &str) -> ReductError {
189        ReductError {
190            status: ErrorCode::Timeout,
191            message: msg.to_string(),
192        }
193    }
194
195    /// Create a no content error.
196    pub fn no_content(msg: &str) -> ReductError {
197        ReductError {
198            status: ErrorCode::NoContent,
199            message: msg.to_string(),
200        }
201    }
202
203    /// Create a not found error.
204    pub fn not_found(msg: &str) -> ReductError {
205        ReductError {
206            status: ErrorCode::NotFound,
207            message: msg.to_string(),
208        }
209    }
210
211    /// Create a conflict error.
212    pub fn conflict(msg: &str) -> ReductError {
213        ReductError {
214            status: ErrorCode::Conflict,
215            message: msg.to_string(),
216        }
217    }
218
219    /// Create a bad request error.
220    pub fn bad_request(msg: &str) -> ReductError {
221        ReductError {
222            status: ErrorCode::BadRequest,
223            message: msg.to_string(),
224        }
225    }
226
227    /// Create an unauthorized error.
228    pub fn unauthorized(msg: &str) -> ReductError {
229        ReductError {
230            status: ErrorCode::Unauthorized,
231            message: msg.to_string(),
232        }
233    }
234
235    /// Create a forbidden error.
236    pub fn forbidden(msg: &str) -> ReductError {
237        ReductError {
238            status: ErrorCode::Forbidden,
239            message: msg.to_string(),
240        }
241    }
242
243    /// Create an unprocessable entity error.
244    pub fn unprocessable_entity(msg: &str) -> ReductError {
245        ReductError {
246            status: ErrorCode::UnprocessableEntity,
247            message: msg.to_string(),
248        }
249    }
250
251    /// Create a too early error.
252    pub fn too_early(msg: &str) -> ReductError {
253        ReductError {
254            status: ErrorCode::TooEarly,
255            message: msg.to_string(),
256        }
257    }
258
259    /// Create a bad request error.
260    pub fn internal_server_error(msg: &str) -> ReductError {
261        ReductError {
262            status: ErrorCode::InternalServerError,
263            message: msg.to_string(),
264        }
265    }
266}
267
268// Macros for creating errors with a message.
269
270#[macro_export]
271macro_rules! timeout {
272    ($msg:expr, $($arg:tt)*) => {
273        ReductError::timeout(&format!($msg, $($arg)*))
274    };
275    ($msg:expr) => {
276        ReductError::timeout($msg)
277    };
278}
279
280#[macro_export]
281macro_rules! no_content {
282    ($msg:expr, $($arg:tt)*) => {
283        ReductError::no_content(&format!($msg, $($arg)*))
284    };
285    ($msg:expr) => {
286        ReductError::no_content($msg)
287    };
288}
289
290#[macro_export]
291macro_rules! bad_request {
292    ($msg:expr, $($arg:tt)*) => {
293        ReductError::bad_request(&format!($msg, $($arg)*))
294    };
295    ($msg:expr) => {
296        ReductError::bad_request($msg)
297    };
298}
299
300#[macro_export]
301macro_rules! forbidden {
302    ($msg:expr, $($arg:tt)*) => {
303        ReductError::forbidden(&format!($msg, $($arg)*))
304    };
305    ($msg:expr) => {
306        ReductError::forbidden($msg)
307    };
308}
309
310#[macro_export]
311macro_rules! unprocessable_entity {
312    ($msg:expr, $($arg:tt)*) => {
313        ReductError::unprocessable_entity(&format!($msg, $($arg)*))
314    };
315    ($msg:expr) => {
316        ReductError::unprocessable_entity($msg)
317    };
318}
319
320#[macro_export]
321macro_rules! not_found {
322    ($msg:expr, $($arg:tt)*) => {
323        ReductError::not_found(&format!($msg, $($arg)*))
324    };
325    ($msg:expr) => {
326        ReductError::not_found($msg)
327    };
328}
329#[macro_export]
330macro_rules! conflict {
331    ($msg:expr, $($arg:tt)*) => {
332        ReductError::conflict(&format!($msg, $($arg)*))
333    };
334    ($msg:expr) => {
335        ReductError::conflict($msg)
336    };
337}
338
339#[macro_export]
340macro_rules! too_early {
341    ($msg:expr, $($arg:tt)*) => {
342        ReductError::too_early(&format!($msg, $($arg)*))
343    };
344    ($msg:expr) => {
345        ReductError::too_early($msg)
346    };
347}
348
349#[macro_export]
350macro_rules! internal_server_error {
351    ($msg:expr, $($arg:tt)*) => {
352        ReductError::internal_server_error(&format!($msg, $($arg)*))
353    };
354    ($msg:expr) => {
355        ReductError::internal_server_error($msg)
356    };
357}
358
359#[macro_export]
360macro_rules! unauthorized {
361    ($msg:expr, $($arg:tt)*) => {
362        ReductError::unauthorized(&format!($msg, $($arg)*))
363    };
364    ($msg:expr) => {
365        ReductError::unauthorized($msg)
366    };
367}
368
369#[macro_export]
370macro_rules! service_unavailable {
371    ($msg:expr, $($arg:tt)*) => {
372        ReductError::new(ErrorCode::ServiceUnavailable, &format!($msg, $($arg)*))
373    };
374    ($msg:expr) => {
375        ReductError::new(ErrorCode::ServiceUnavailable, $msg)
376    };
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use std::time::{SystemTime, UNIX_EPOCH};
383
384    #[test]
385    fn creates_internal_server_error() {
386        let error = ReductError::internal_server_error("Unexpected server error");
387        assert_eq!(error.status, ErrorCode::InternalServerError);
388        assert_eq!(error.message, "Unexpected server error");
389    }
390
391    #[test]
392    fn converts_io_error_to_reduct_error() {
393        let io_error = std::io::Error::new(std::io::ErrorKind::Other, "IO failure");
394        let error: ReductError = io_error.into();
395        assert_eq!(error.status, ErrorCode::InternalServerError);
396        assert_eq!(error.message, "IO failure");
397    }
398
399    #[test]
400    fn converts_system_time_error_to_reduct_error() {
401        let system_time_error = UNIX_EPOCH.duration_since(SystemTime::now()).unwrap_err();
402        let error: ReductError = system_time_error.into();
403        assert_eq!(error.status, ErrorCode::InternalServerError);
404        assert_eq!(error.message, "second time provided was later than self");
405    }
406
407    #[test]
408    fn converts_url_parse_error_to_reduct_error() {
409        let parse_error = ParseError::EmptyHost;
410        let error: ReductError = parse_error.into();
411        assert_eq!(error.status, ErrorCode::UrlParseError);
412        assert_eq!(error.message, "empty host");
413    }
414
415    #[test]
416    fn converts_poison_error_to_reduct_error() {
417        let poison_error: PoisonError<()> = PoisonError::new(());
418        let error: ReductError = poison_error.into();
419        assert_eq!(error.status, ErrorCode::InternalServerError);
420        assert_eq!(error.message, "Poison error");
421    }
422
423    #[cfg(feature = "io")]
424    #[test]
425    fn converts_send_error_to_reduct_error() {
426        let send_error: SendError<()> = SendError(());
427        let error: ReductError = send_error.into();
428        assert_eq!(error.status, ErrorCode::InternalServerError);
429        assert_eq!(error.message, "channel closed");
430    }
431
432    mod macros {
433        use super::*;
434
435        #[test]
436        fn test_timeout_macro() {
437            let error = timeout!("Timeout error: {}", 42);
438            assert_eq!(error.status, ErrorCode::Timeout);
439            assert_eq!(error.message, "Timeout error: 42");
440        }
441
442        #[test]
443        fn test_no_content_macro() {
444            let error = no_content!("No content error: {}", 42);
445            assert_eq!(error.status, ErrorCode::NoContent);
446            assert_eq!(error.message, "No content error: 42");
447        }
448
449        #[test]
450        fn test_bad_request_macro() {
451            let error = bad_request!("Bad request error: {}", 42);
452            assert_eq!(error.status, ErrorCode::BadRequest);
453            assert_eq!(error.message, "Bad request error: 42");
454        }
455
456        #[test]
457        fn test_unprocessable_entity_macro() {
458            let error = unprocessable_entity!("Unprocessable entity error: {}", 42);
459            assert_eq!(error.status, ErrorCode::UnprocessableEntity);
460            assert_eq!(error.message, "Unprocessable entity error: 42");
461        }
462
463        #[test]
464        fn test_not_found_macro() {
465            let error = not_found!("Not found error: {}", 42);
466            assert_eq!(error.status, ErrorCode::NotFound);
467            assert_eq!(error.message, "Not found error: 42");
468        }
469
470        #[test]
471        fn test_conflict_macro() {
472            let error = conflict!("Conflict error: {}", 42);
473            assert_eq!(error.status, ErrorCode::Conflict);
474            assert_eq!(error.message, "Conflict error: 42");
475        }
476
477        #[test]
478        fn test_too_early_macro() {
479            let error = too_early!("Too early error: {}", 42);
480            assert_eq!(error.status, ErrorCode::TooEarly);
481            assert_eq!(error.message, "Too early error: 42");
482        }
483
484        #[test]
485        fn test_internal_server_error_macro() {
486            let error = internal_server_error!("Internal server error: {}", 42);
487            assert_eq!(error.status, ErrorCode::InternalServerError);
488            assert_eq!(error.message, "Internal server error: 42");
489        }
490    }
491}