Skip to main content

ntex_error/
lib.rs

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