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 chain;
14mod error;
15mod ext;
16mod info;
17mod message;
18mod repr;
19pub mod utils;
20
21pub use crate::bt::{Backtrace, BacktraceResolver};
22pub use crate::chain::ErrorChain;
23pub use crate::error::Error;
24pub use crate::info::ErrorInfo;
25pub use crate::message::{ErrorMessage, ErrorMessageChained};
26pub use crate::message::{fmt_diag, fmt_diag_string, fmt_err, fmt_err_string};
27pub use crate::utils::{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)]
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
51/// Describes how a domain-specific error maps into a unified result model.
52pub trait ResultKind: 'static {
53    /// Returns the classification of the result (e.g. success, client error, service error).
54    fn tp(&self) -> ResultType;
55
56    /// Returns a stable identifier for the specific error classification.
57    ///
58    /// It is used for logging, metrics, and diagnostics.
59    fn signature(&self) -> &'static str;
60}
61
62impl ResultKind for ResultType {
63    fn tp(&self) -> ResultType {
64        *self
65    }
66
67    fn signature(&self) -> &'static str {
68        self.as_str()
69    }
70}
71
72/// Provides diagnostic information for errors.
73///
74/// It enables classification, service attribution, and debugging context.
75pub trait ErrorDiagnostic: StdError + 'static {
76    type Kind: ResultKind;
77
78    /// Returns the classification kind of this error.
79    fn kind(&self) -> Self::Kind;
80
81    /// Returns an optional tag associated with this error.
82    ///
83    /// The tag is user-defined and can be used for additional classification
84    /// or correlation.
85    fn tag(&self) -> Option<&Bytes> {
86        None
87    }
88
89    /// Returns the name of the responsible service, if applicable.
90    ///
91    /// Used to identify upstream or internal service ownership for diagnostics.
92    fn service(&self) -> Option<&'static str> {
93        None
94    }
95
96    /// Returns a backtrace for debugging purposes, if available.
97    fn backtrace(&self) -> Option<&Backtrace> {
98        None
99    }
100
101    #[track_caller]
102    fn chain(self) -> ErrorChain<Self::Kind>
103    where
104        Self: Sized,
105    {
106        ErrorChain::new(self)
107    }
108}
109
110/// Helper trait for converting a value into a unified error-aware result type.
111pub trait ErrorMapping<T, E, U> {
112    /// Converts the value into a `Result`, wrapping it in a structured error type if needed.
113    fn into_error(self) -> Result<T, Error<U>>;
114}
115
116impl fmt::Display for ResultType {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "{}", self.as_str())
119    }
120}
121
122impl<T, E> ResultKind for Result<T, E>
123where
124    T: 'static,
125    E: ErrorDiagnostic + 'static,
126{
127    fn tp(&self) -> ResultType {
128        match self {
129            Ok(_) => ResultType::Success,
130            Err(e) => e.kind().tp(),
131        }
132    }
133
134    fn signature(&self) -> &'static str {
135        match self {
136            Ok(_) => ResultType::Success.as_str(),
137            Err(e) => e.kind().signature(),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use std::{error::Error as StdError, io, mem};
145
146    use super::*;
147
148    #[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
149    enum TestKind {
150        #[error("Connect")]
151        Connect,
152        #[error("Disconnect")]
153        Disconnect,
154        #[error("ServiceError")]
155        ServiceError,
156    }
157
158    impl ResultKind for TestKind {
159        fn tp(&self) -> ResultType {
160            match self {
161                TestKind::Connect | TestKind::Disconnect => ResultType::ClientError,
162                TestKind::ServiceError => ResultType::ServiceError,
163            }
164        }
165
166        fn signature(&self) -> &'static str {
167            match self {
168                TestKind::Connect => "Client-Connect",
169                TestKind::Disconnect => "Client-Disconnect",
170                TestKind::ServiceError => "Service-Internal",
171            }
172        }
173    }
174
175    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
176    enum TestError {
177        #[error("Connect err: {0}")]
178        Connect(&'static str),
179        #[error("Disconnect")]
180        Disconnect,
181        #[error("InternalServiceError")]
182        Service(&'static str),
183    }
184
185    impl ErrorDiagnostic for TestError {
186        type Kind = TestKind;
187
188        fn kind(&self) -> Self::Kind {
189            match self {
190                TestError::Connect(_) => TestKind::Connect,
191                TestError::Disconnect => TestKind::Disconnect,
192                TestError::Service(_) => TestKind::ServiceError,
193            }
194        }
195
196        fn service(&self) -> Option<&'static str> {
197            Some("test")
198        }
199    }
200
201    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
202    #[error("TestError2")]
203    struct TestError2;
204    impl ErrorDiagnostic for TestError2 {
205        type Kind = ResultType;
206
207        fn kind(&self) -> Self::Kind {
208            ResultType::ClientError
209        }
210    }
211
212    impl From<TestError> for TestError2 {
213        fn from(_err: TestError) -> TestError2 {
214            TestError2
215        }
216    }
217
218    #[ntex::test]
219    async fn test_error() {
220        let err: Error<TestError> = TestError::Service("409 Error").into();
221        let err = err.clone();
222        assert_eq!(err.kind(), TestKind::ServiceError);
223        assert_eq!((*err).kind(), TestKind::ServiceError);
224        assert_eq!(err.to_string(), "InternalServiceError");
225        assert_eq!(err.service(), Some("test"));
226        assert_eq!(err.kind().signature(), "Service-Internal");
227        assert_eq!(
228            err,
229            Into::<Error<TestError>>::into(TestError::Service("409 Error"))
230        );
231        assert!(err.backtrace().is_some());
232
233        let err = err.set_service("SVC");
234        assert_eq!(err.service(), Some("SVC"));
235        let err = err.set_tag("TAG");
236        assert_eq!(err.tag().unwrap(), &b"TAG"[..]);
237
238        let err2: Error<TestError> = Error::new(TestError::Service("409 Error"), "TEST");
239        assert!(err != err2);
240        assert_eq!(err, TestError::Service("409 Error"));
241
242        let err2 = err2.set_tag("TAG");
243        assert_eq!(err.tag().unwrap(), &b"TAG"[..]);
244        let err2 = err2.set_service("SVC");
245        assert_eq!(err, err2);
246        let err2 = err2.map(|_| TestError::Disconnect);
247        assert!(err != err2);
248        let err2 = err2.forward(|_| TestError::Disconnect);
249        assert!(err != err2);
250
251        assert_eq!(TestError::Connect("").kind().tp(), ResultType::ClientError);
252        assert_eq!(TestError::Disconnect.kind().tp(), ResultType::ClientError);
253        assert_eq!(TestError::Service("").kind().tp(), ResultType::ServiceError);
254        assert_eq!(TestError::Connect("").to_string(), "Connect err: ");
255        assert_eq!(TestError::Disconnect.to_string(), "Disconnect");
256        assert_eq!(TestError::Disconnect.service(), Some("test"));
257        assert!(TestError::Disconnect.backtrace().is_none());
258
259        assert_eq!(ResultType::ClientError.as_str(), "ClientError");
260        assert_eq!(ResultType::ServiceError.as_str(), "ServiceError");
261        assert_eq!(ResultType::ClientError.tp(), ResultType::ClientError);
262        assert_eq!(ResultType::ServiceError.tp(), ResultType::ServiceError);
263        assert_eq!(ResultType::ClientError.to_string(), "ClientError");
264        assert_eq!(ResultType::ServiceError.to_string(), "ServiceError");
265        assert_eq!(format!("{}", ResultType::ClientError), "ClientError");
266
267        assert_eq!(TestKind::Connect.to_string(), "Connect");
268        assert_eq!(TestError::Connect("").kind().signature(), "Client-Connect");
269        assert_eq!(TestKind::Disconnect.to_string(), "Disconnect");
270        assert_eq!(
271            TestError::Disconnect.kind().signature(),
272            "Client-Disconnect"
273        );
274        assert_eq!(TestKind::ServiceError.to_string(), "ServiceError");
275        assert_eq!(
276            TestError::Service("").kind().signature(),
277            "Service-Internal"
278        );
279
280        let err = err.into_error().chain();
281        assert_eq!(err.kind(), TestKind::ServiceError);
282        assert_eq!(err.kind(), TestError::Service("409 Error").kind());
283        assert_eq!(err.to_string(), "InternalServiceError");
284        assert!(format!("{err:?}").contains("Service(\"409 Error\")"));
285        assert!(
286            format!("{:?}", err.source()).contains("Service(\"409 Error\")"),
287            "{:?}",
288            err.source().unwrap()
289        );
290
291        let err: Error<TestError> = TestError::Service("404 Error").into();
292        #[cfg(unix)]
293        {
294            if let Some(bt) = err.backtrace() {
295                bt.resolver().resolve();
296                assert!(
297                    format!("{bt}").contains("ntex_error::tests::test_error"),
298                    "{bt}",
299                );
300                assert!(
301                    bt.repr().unwrap().contains("ntex_error::tests::test_error"),
302                    "{bt}"
303                );
304            }
305        }
306
307        let err: ErrorChain<TestKind> = err.into();
308        assert_eq!(err.kind(), TestKind::ServiceError);
309        assert_eq!(err.kind(), TestError::Service("404 Error").kind());
310        assert_eq!(err.service(), Some("test"));
311        assert_eq!(err.kind().signature(), "Service-Internal");
312        assert_eq!(err.to_string(), "InternalServiceError");
313        assert!(err.backtrace().is_some());
314        assert!(format!("{err:?}").contains("Service(\"404 Error\")"));
315
316        assert_eq!(24, mem::size_of::<TestError>());
317        assert_eq!(8, mem::size_of::<Error<TestError>>());
318
319        assert_eq!(TestError2.service(), None);
320        assert_eq!(TestError2.kind().signature(), "ClientError");
321
322        // ErrorInformation
323        let err: Error<TestError> = TestError::Service("409 Error").into();
324        let msg = fmt_err_string(&err);
325        assert_eq!(msg, "InternalServiceError\n");
326        let msg = fmt_diag_string(&err);
327        assert!(msg.contains("err: InternalServiceError"));
328
329        let err: ErrorInfo = err.set_service("SVC").into();
330        assert_eq!(err.tp(), ResultType::ServiceError);
331        assert_eq!(err.service(), Some("SVC"));
332        assert_eq!(err.signature(), "Service-Internal");
333        assert!(err.backtrace().is_some());
334
335        let res = Err(TestError::Service("409 Error"));
336        let res: Result<(), Error<TestError>> = res.into_error();
337        let _res: Result<(), Error<TestError2>> = res.into_error();
338
339        let msg = fmt_err_string(&err);
340        assert_eq!(msg, "InternalServiceError\n");
341        let msg = fmt_diag_string(&err);
342        assert!(msg.contains("err: InternalServiceError"));
343
344        // Error extensions
345        let err: Error<TestError> = TestError::Service("409 Error").into();
346        assert_eq!(err.get_item::<&str>(), None);
347        let err = err.insert_item("Test");
348        assert_eq!(err.get_item::<&str>(), Some(&"Test"));
349        let err2 = err.clone();
350        assert_eq!(err2.get_item::<&str>(), Some(&"Test"));
351        let err2 = err2.insert_item("Test2");
352        assert_eq!(err2.get_item::<&str>(), Some(&"Test2"));
353        assert_eq!(err.get_item::<&str>(), Some(&"Test"));
354        let err2 = err.clone().map(|_| TestError::Disconnect);
355        assert_eq!(err2.get_item::<&str>(), Some(&"Test"));
356
357        let info = ErrorInfo::from(&err2);
358        assert_eq!(info.get_item::<&str>(), Some(&"Test"));
359
360        let err3 = err
361            .clone()
362            .try_map(|_| Err::<(), _>(TestError2))
363            .err()
364            .unwrap();
365        assert_eq!(err3.kind().signature(), "ClientError");
366        assert_eq!(err3.get_item::<&str>(), Some(&"Test"));
367
368        let res = err.clone().try_map(|_| Ok::<_, TestError2>(()));
369        assert_eq!(res, Ok(()));
370
371        assert_eq!(Success.kind(), ResultType::Success);
372        assert_eq!(format!("{Success}"), "Success");
373
374        assert_eq!(
375            ErrorDiagnostic::kind(&io::Error::other("")),
376            ResultType::ServiceError
377        );
378        assert_eq!(
379            ErrorDiagnostic::kind(&io::Error::new(io::ErrorKind::InvalidData, "")),
380            ResultType::ClientError
381        );
382
383        let res = Ok::<_, TestError>(());
384        assert_eq!(res.tp(), ResultType::Success);
385
386        let res = Err::<(), _>(TestError::Service("409 Error"));
387        assert_eq!(res.tp(), ResultType::ServiceError);
388        assert_eq!(res.signature(), "Service-Internal");
389    }
390}