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