Skip to main content

ntex_error/
lib.rs

1//! Error management.
2#![deny(clippy::pedantic)]
3#![allow(
4    clippy::must_use_candidate,
5    clippy::missing_errors_doc,
6    clippy::missing_panics_doc
7)]
8use std::{error::Error as StdError, fmt};
9
10use ntex_bytes::Bytes;
11
12mod bt;
13mod error;
14mod ext;
15mod info;
16mod message;
17mod repr;
18pub mod utils;
19
20pub use crate::bt::{Backtrace, BacktraceResolver};
21pub use crate::error::Error;
22pub use crate::info::ErrorInfo;
23pub use crate::message::{ErrorMessage, ErrorMessageChained};
24pub use crate::message::{fmt_diag, fmt_diag_string, fmt_err, fmt_err_string};
25pub use crate::utils::{ResultInfo, Success, with_service};
26
27#[doc(hidden)]
28pub use crate::bt::{set_backtrace_start, set_backtrace_start_alt};
29
30/// The type of the result.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
32pub enum ResultType {
33    Success,
34    ClientError,
35    ServiceError,
36}
37
38impl ResultType {
39    /// Returns a str representation of the result type.
40    pub const fn as_str(&self) -> &'static str {
41        match self {
42            ResultType::Success => "Success",
43            ResultType::ClientError => "ClientError",
44            ResultType::ServiceError => "ServiceError",
45        }
46    }
47}
48
49/// Provides diagnostic information for errors.
50///
51/// It enables classification, service attribution, and debugging context.
52pub trait ErrorDiagnostic: StdError + 'static {
53    /// Returns the classification of the result (e.g. success, client error, service error).
54    fn typ(&self) -> ResultType {
55        ResultType::ServiceError
56    }
57
58    /// Returns a stable identifier for the specific error classification.
59    ///
60    /// It is used for logging, metrics, and diagnostics.
61    fn signature(&self) -> &'static str;
62
63    /// Returns an optional tag associated with this error.
64    ///
65    /// The tag is user-defined and can be used for additional classification
66    /// or correlation.
67    fn tag(&self) -> Option<&Bytes> {
68        None
69    }
70
71    /// Returns the name of the responsible service, if applicable.
72    ///
73    /// Used to identify upstream or internal service ownership for diagnostics.
74    fn service(&self) -> Option<&'static str> {
75        None
76    }
77
78    /// Returns a backtrace for debugging purposes, if available.
79    fn backtrace(&self) -> Option<&Backtrace> {
80        None
81    }
82}
83
84/// Helper trait for converting a value into a unified error-aware result type.
85pub trait ErrorMapping<T, E, U> {
86    /// Converts the value into a `Result`, wrapping it in a structured error type if needed.
87    fn into_error(self) -> Result<T, Error<U>>;
88}
89
90impl fmt::Display for ResultType {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{}", self.as_str())
93    }
94}
95
96impl ErrorDiagnostic for ResultType {
97    fn typ(&self) -> ResultType {
98        *self
99    }
100
101    fn signature(&self) -> &'static str {
102        self.as_str()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::{error::Error as StdError, io, mem};
109
110    use super::*;
111
112    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
113    enum TestError {
114        #[error("Connect err: {0}")]
115        Connect(&'static str),
116        #[error("Disconnect")]
117        Disconnect,
118        #[error("InternalServiceError")]
119        Service(&'static str),
120    }
121
122    impl ErrorDiagnostic for TestError {
123        fn typ(&self) -> ResultType {
124            match self {
125                TestError::Connect(_) | TestError::Disconnect => ResultType::ClientError,
126                TestError::Service(_) => ResultType::ServiceError,
127            }
128        }
129
130        fn signature(&self) -> &'static str {
131            match self {
132                TestError::Connect(_) => "Client-Connect",
133                TestError::Disconnect => "Client-Disconnect",
134                TestError::Service(_) => "Service-Internal",
135            }
136        }
137
138        fn service(&self) -> Option<&'static str> {
139            Some("test")
140        }
141    }
142
143    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
144    #[error("TestError2")]
145    struct TestError2;
146    impl ErrorDiagnostic for TestError2 {
147        fn typ(&self) -> ResultType {
148            ResultType::ClientError
149        }
150
151        fn signature(&self) -> &'static str {
152            "TestError2"
153        }
154    }
155
156    impl From<TestError> for TestError2 {
157        fn from(_err: TestError) -> TestError2 {
158            TestError2
159        }
160    }
161
162    #[ntex::test]
163    async fn test_error() {
164        let err: Error<TestError> = TestError::Service("409 Error").into();
165        let err = err.clone();
166        assert_eq!(err.typ(), ResultType::ServiceError);
167        assert_eq!((*err).typ(), ResultType::ServiceError);
168        assert_eq!(err.to_string(), "InternalServiceError");
169        assert_eq!(err.service(), Some("test"));
170        assert_eq!(err.signature(), "Service-Internal");
171        assert_eq!(
172            err,
173            Into::<Error<TestError>>::into(TestError::Service("409 Error"))
174        );
175        assert!(err.backtrace().is_some());
176
177        let err = err.set_service("SVC");
178        assert_eq!(err.service(), Some("SVC"));
179        let err = err.set_tag("TAG");
180        assert_eq!(err.tag().unwrap(), &b"TAG"[..]);
181
182        let err2: Error<TestError> = Error::new(TestError::Service("409 Error"), "TEST");
183        assert!(err != err2);
184        assert_eq!(err, TestError::Service("409 Error"));
185
186        let err2 = err2.set_tag("TAG");
187        assert_eq!(err.tag().unwrap(), &b"TAG"[..]);
188        let err2 = err2.set_service("SVC");
189        assert_eq!(err, err2);
190        let err2 = err2.map(|_| TestError::Disconnect);
191        assert!(err != err2);
192        let err2 = err2.forward(|_| TestError::Disconnect);
193        assert!(err != err2);
194
195        assert_eq!(TestError::Connect("").typ(), ResultType::ClientError);
196        assert_eq!(TestError::Disconnect.typ(), ResultType::ClientError);
197        assert_eq!(TestError::Service("").typ(), ResultType::ServiceError);
198        assert_eq!(TestError::Connect("").to_string(), "Connect err: ");
199        assert_eq!(TestError::Disconnect.to_string(), "Disconnect");
200        assert_eq!(TestError::Disconnect.service(), Some("test"));
201        assert!(TestError::Disconnect.backtrace().is_none());
202
203        assert_eq!(ResultType::ClientError.as_str(), "ClientError");
204        assert_eq!(ResultType::ServiceError.as_str(), "ServiceError");
205        assert_eq!(ResultType::ClientError.typ(), ResultType::ClientError);
206        assert_eq!(ResultType::ServiceError.typ(), ResultType::ServiceError);
207        assert_eq!(ResultType::ClientError.to_string(), "ClientError");
208        assert_eq!(ResultType::ServiceError.to_string(), "ServiceError");
209        assert_eq!(format!("{}", ResultType::ClientError), "ClientError");
210
211        assert_eq!(TestError::Connect("").signature(), "Client-Connect");
212        assert_eq!(TestError::Disconnect.signature(), "Client-Disconnect");
213        assert_eq!(TestError::Service("").signature(), "Service-Internal");
214
215        let err = err.into_error();
216        assert_eq!(err.typ(), ResultType::ServiceError);
217        assert_eq!(err.to_string(), "InternalServiceError");
218        assert!(err.source().is_none());
219        assert!(format!("{err:?}").contains("Service(\"409 Error\")"));
220
221        #[cfg(unix)]
222        {
223            let err: Error<TestError> = TestError::Service("404 Error").into();
224            if let Some(bt) = err.backtrace() {
225                bt.resolver().resolve();
226                assert!(
227                    format!("{bt}").contains("ntex_error::tests::test_error"),
228                    "{bt}",
229                );
230                assert!(
231                    bt.repr().unwrap().contains("ntex_error::tests::test_error"),
232                    "{bt}"
233                );
234            }
235        }
236
237        assert_eq!(24, mem::size_of::<TestError>());
238        assert_eq!(8, mem::size_of::<Error<TestError>>());
239
240        assert_eq!(TestError2.service(), None);
241        assert_eq!(TestError2.signature(), "TestError2");
242
243        // ErrorInformation
244        let err: Error<TestError> = TestError::Service("409 Error").into();
245        let msg = fmt_err_string(&err);
246        assert_eq!(msg, "InternalServiceError\n");
247        let msg = fmt_diag_string(&err);
248        assert!(msg.contains("err: InternalServiceError"));
249
250        let err: ErrorInfo = err.set_service("SVC").into();
251        assert_eq!(err.typ(), ResultType::ServiceError);
252        assert_eq!(err.service(), Some("SVC"));
253        assert_eq!(err.signature(), "Service-Internal");
254        assert!(err.backtrace().is_some());
255
256        let res = Err(TestError::Service("409 Error"));
257        let res: Result<(), Error<TestError>> = res.into_error();
258        let _res: Result<(), Error<TestError2>> = res.into_error();
259
260        let msg = fmt_err_string(&err);
261        assert_eq!(msg, "InternalServiceError\n");
262        let msg = fmt_diag_string(&err);
263        assert!(msg.contains("err: InternalServiceError"));
264
265        // Error extensions
266        let err: Error<TestError> = TestError::Service("409 Error").into();
267        assert_eq!(err.get_item::<&str>(), None);
268        let err = err.insert_item("Test");
269        assert_eq!(err.get_item::<&str>(), Some(&"Test"));
270        let err2 = err.clone();
271        assert_eq!(err2.get_item::<&str>(), Some(&"Test"));
272        let err2 = err2.insert_item("Test2");
273        assert_eq!(err2.get_item::<&str>(), Some(&"Test2"));
274        assert_eq!(err.get_item::<&str>(), Some(&"Test"));
275        let err2 = err.clone().map(|_| TestError::Disconnect);
276        assert_eq!(err2.get_item::<&str>(), Some(&"Test"));
277
278        let info = ErrorInfo::from(&err2);
279        assert_eq!(info.get_item::<&str>(), Some(&"Test"));
280
281        let err3 = err
282            .clone()
283            .try_map(|_| Err::<(), _>(TestError2))
284            .err()
285            .unwrap();
286        assert_eq!(err3.signature(), "TestError2");
287        assert_eq!(err3.get_item::<&str>(), Some(&"Test"));
288
289        let res = err.clone().try_map(|_| Ok::<_, TestError2>(()));
290        assert_eq!(res, Ok(()));
291
292        assert_eq!(Success.typ(), ResultType::Success);
293        assert_eq!(format!("{Success}"), "Success");
294
295        assert_eq!(io::Error::other("").typ(), ResultType::ServiceError);
296        assert_eq!(
297            io::Error::new(io::ErrorKind::InvalidData, "").typ(),
298            ResultType::ClientError
299        );
300
301        let res = Ok::<_, TestError>(());
302        let info = ResultInfo::from(&res);
303        assert_eq!(info.typ(), ResultType::Success);
304        assert_eq!(info.signature(), "Success");
305
306        let res = Err::<(), _>(TestError::Service("409 Error"));
307        let info = ResultInfo::from(&res);
308        assert_eq!(info.typ(), ResultType::ServiceError);
309        assert_eq!(info.signature(), "Service-Internal");
310    }
311}