recoco_utils/
error.rs

1// ReCoco is a Rust-only fork of CocoIndex, by [CocoIndex](https://CocoIndex)
2// Original code from CocoIndex is copyrighted by CocoIndex
3// SPDX-FileCopyrightText: 2025-2026 CocoIndex (upstream)
4// SPDX-FileContributor: CocoIndex Contributors
5//
6// All modifications from the upstream for ReCoco are copyrighted by Knitli Inc.
7// SPDX-FileCopyrightText: 2026 Knitli Inc. (ReCoco)
8// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
9//
10// Both the upstream CocoIndex code and the ReCoco modifications are licensed under the Apache-2.0 License.
11// SPDX-License-Identifier: Apache-2.0
12
13use axum::{
14    Json,
15    http::StatusCode,
16    response::{IntoResponse, Response},
17};
18use serde::Serialize;
19use std::{
20    any::Any,
21    backtrace::Backtrace,
22    error::Error as StdError,
23    fmt::{Debug, Display},
24    sync::{Arc, Mutex},
25};
26
27pub trait HostError: Any + StdError + Send + Sync + 'static {}
28impl<T: Any + StdError + Send + Sync + 'static> HostError for T {}
29
30pub enum Error {
31    Context { msg: String, source: Box<SError> },
32    HostLang(Box<dyn HostError>),
33    Client { msg: String, bt: Backtrace },
34    Internal(anyhow::Error),
35}
36
37impl Display for Error {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self.format_context(f)? {
40            Error::Context { .. } => Ok(()),
41            Error::HostLang(e) => write!(f, "{}", e),
42            Error::Client { msg, .. } => write!(f, "Invalid Request: {}", msg),
43            Error::Internal(e) => write!(f, "{}", e),
44        }
45    }
46}
47impl Debug for Error {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self.format_context(f)? {
50            Error::Context { .. } => Ok(()),
51            Error::HostLang(e) => write!(f, "{:?}", e),
52            Error::Client { msg, bt } => {
53                write!(f, "Invalid Request: {msg}\n\n{bt}\n")
54            }
55            Error::Internal(e) => write!(f, "{e:?}"),
56        }
57    }
58}
59
60pub type Result<T, E = Error> = std::result::Result<T, E>;
61
62// Backwards compatibility aliases
63pub type CError = Error;
64pub type CResult<T> = Result<T>;
65
66impl Error {
67    pub fn host(e: impl HostError) -> Self {
68        Self::HostLang(Box::new(e))
69    }
70
71    pub fn client(msg: impl Into<String>) -> Self {
72        Self::Client {
73            msg: msg.into(),
74            bt: Backtrace::capture(),
75        }
76    }
77
78    pub fn internal(e: impl Into<anyhow::Error>) -> Self {
79        Self::Internal(e.into())
80    }
81
82    pub fn internal_msg(msg: impl Into<String>) -> Self {
83        Self::Internal(anyhow::anyhow!("{}", msg.into()))
84    }
85
86    pub fn backtrace(&self) -> Option<&Backtrace> {
87        match self {
88            Error::Client { bt, .. } => Some(bt),
89            Error::Internal(e) => Some(e.backtrace()),
90            Error::Context { source, .. } => source.0.backtrace(),
91            Error::HostLang(_) => None,
92        }
93    }
94
95    pub fn without_contexts(&self) -> &Error {
96        match self {
97            Error::Context { source, .. } => source.0.without_contexts(),
98            other => other,
99        }
100    }
101
102    pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
103        match self {
104            Error::Context { source, .. } => Some(source.as_ref()),
105            Error::HostLang(e) => Some(e.as_ref()),
106            Error::Internal(e) => e.source(),
107            Error::Client { .. } => None,
108        }
109    }
110
111    pub fn context<C: Into<String>>(self, context: C) -> Self {
112        Self::Context {
113            msg: context.into(),
114            source: Box::new(SError(self)),
115        }
116    }
117
118    pub fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Self {
119        Self::Context {
120            msg: f().into(),
121            source: Box::new(SError(self)),
122        }
123    }
124
125    pub fn std_error(self) -> SError {
126        SError(self)
127    }
128
129    fn format_context(&self, f: &mut std::fmt::Formatter<'_>) -> Result<&Error, std::fmt::Error> {
130        let mut current = self;
131        if matches!(current, Error::Context { .. }) {
132            write!(f, "\nContext:\n")?;
133            let mut next_id = 1;
134            while let Error::Context { msg, source } = current {
135                writeln!(f, "  {next_id}: {msg}")?;
136                current = source.inner();
137                next_id += 1;
138            }
139        }
140        Ok(current)
141    }
142}
143
144impl StdError for Error {
145    fn source(&self) -> Option<&(dyn StdError + 'static)> {
146        self.source()
147    }
148}
149
150// impl<E: Into<anyhow::Error>> From<E> for Error {
151//     fn from(e: E) -> Self {
152//         Error::Internal(e.into())
153//     }
154// }
155
156// impl<E: Into<anyhow::Error>> From<E> for Error {
157//     fn from(e: E) -> Self {
158//         Error::Internal(e.into())
159//     }
160// }
161
162// Explicitly implement From for common error types used in recoco_utils to avoid conflict with From<T> for T
163impl From<anyhow::Error> for Error {
164    fn from(e: anyhow::Error) -> Self {
165        Error::Internal(e)
166    }
167}
168
169impl From<std::io::Error> for Error {
170    fn from(e: std::io::Error) -> Self {
171        Error::Internal(e.into())
172    }
173}
174
175impl From<tokio::task::JoinError> for Error {
176    fn from(e: tokio::task::JoinError) -> Self {
177        Error::Internal(e.into())
178    }
179}
180
181impl From<tokio::sync::oneshot::error::RecvError> for Error {
182    fn from(e: tokio::sync::oneshot::error::RecvError) -> Self {
183        Error::Internal(e.into())
184    }
185}
186
187impl From<base64::DecodeError> for Error {
188    fn from(e: base64::DecodeError) -> Self {
189        Error::Internal(e.into())
190    }
191}
192
193impl From<hex::FromHexError> for Error {
194    fn from(e: hex::FromHexError) -> Self {
195        Error::Internal(e.into())
196    }
197}
198
199impl From<ResidualError> for Error {
200    fn from(e: ResidualError) -> Self {
201        Error::Internal(anyhow::Error::from(e))
202    }
203}
204
205impl From<crate::fingerprint::FingerprinterError> for Error {
206    fn from(e: crate::fingerprint::FingerprinterError) -> Self {
207        Error::Internal(anyhow::Error::new(e))
208    }
209}
210
211impl From<ApiError> for Error {
212    fn from(e: ApiError) -> Self {
213        Error::Internal(e.err)
214    }
215}
216
217impl From<serde_json::Error> for Error {
218    fn from(e: serde_json::Error) -> Self {
219        Error::Internal(e.into())
220    }
221}
222
223impl From<globset::Error> for Error {
224    fn from(e: globset::Error) -> Self {
225        Error::Internal(e.into())
226    }
227}
228
229impl From<regex::Error> for Error {
230    fn from(e: regex::Error) -> Self {
231        Error::Internal(e.into())
232    }
233}
234
235impl<T> From<std::sync::PoisonError<T>> for Error {
236    fn from(e: std::sync::PoisonError<T>) -> Self {
237        Error::Internal(anyhow::anyhow!("Mutex poison error: {}", e))
238    }
239}
240
241impl From<chrono::ParseError> for Error {
242    fn from(e: chrono::ParseError) -> Self {
243        Error::Internal(e.into())
244    }
245}
246
247impl From<uuid::Error> for Error {
248    fn from(e: uuid::Error) -> Self {
249        Error::Internal(e.into())
250    }
251}
252
253impl From<http::header::InvalidHeaderValue> for Error {
254    fn from(e: http::header::InvalidHeaderValue) -> Self {
255        Error::Internal(e.into())
256    }
257}
258
259impl From<std::num::ParseIntError> for Error {
260    fn from(e: std::num::ParseIntError) -> Self {
261        Error::Internal(e.into())
262    }
263}
264
265impl From<std::str::ParseBoolError> for Error {
266    fn from(e: std::str::ParseBoolError) -> Self {
267        Error::Internal(e.into())
268    }
269}
270
271impl From<std::fmt::Error> for Error {
272    fn from(e: std::fmt::Error) -> Self {
273        Error::Internal(e.into())
274    }
275}
276
277impl From<std::string::FromUtf8Error> for Error {
278    fn from(e: std::string::FromUtf8Error) -> Self {
279        Error::Internal(e.into())
280    }
281}
282
283impl From<std::borrow::Cow<'_, str>> for Error {
284    fn from(e: std::borrow::Cow<'_, str>) -> Self {
285        Error::Internal(anyhow::anyhow!("{}", e))
286    }
287}
288
289impl From<tokio::sync::AcquireError> for Error {
290    fn from(e: tokio::sync::AcquireError) -> Self {
291        Error::Internal(e.into())
292    }
293}
294
295impl From<tokio::sync::watch::error::RecvError> for Error {
296    fn from(e: tokio::sync::watch::error::RecvError) -> Self {
297        Error::Internal(e.into())
298    }
299}
300
301#[cfg(feature = "yaml")]
302impl From<yaml_rust2::EmitError> for Error {
303    fn from(e: yaml_rust2::EmitError) -> Self {
304        Error::Internal(e.into())
305    }
306}
307
308#[cfg(feature = "yaml")]
309impl From<crate::yaml_ser::YamlSerializerError> for Error {
310    fn from(e: crate::yaml_ser::YamlSerializerError) -> Self {
311        Error::Internal(anyhow::Error::new(e))
312    }
313}
314
315#[cfg(feature = "reqwest")]
316impl From<reqwest::Error> for Error {
317    fn from(e: reqwest::Error) -> Self {
318        Error::Internal(e.into())
319    }
320}
321
322#[cfg(feature = "sqlx")]
323impl From<sqlx::Error> for Error {
324    fn from(e: sqlx::Error) -> Self {
325        Error::Internal(e.into())
326    }
327}
328
329#[cfg(feature = "neo4rs")]
330impl From<neo4rs::Error> for Error {
331    fn from(e: neo4rs::Error) -> Self {
332        Error::Internal(e.into())
333    }
334}
335
336#[cfg(feature = "openai")]
337impl From<async_openai::error::OpenAIError> for Error {
338    fn from(e: async_openai::error::OpenAIError) -> Self {
339        Error::Internal(e.into())
340    }
341}
342
343#[cfg(feature = "qdrant")]
344impl From<qdrant_client::QdrantError> for Error {
345    fn from(e: qdrant_client::QdrantError) -> Self {
346        Error::Internal(anyhow::Error::msg(e.to_string()))
347    }
348}
349
350#[cfg(feature = "redis")]
351impl From<redis::RedisError> for Error {
352    fn from(e: redis::RedisError) -> Self {
353        Error::Internal(e.into())
354    }
355}
356
357#[cfg(feature = "azure")]
358impl From<azure_storage::Error> for Error {
359    fn from(e: azure_storage::Error) -> Self {
360        Error::Internal(anyhow::Error::msg(e.to_string()))
361    }
362}
363
364#[cfg(feature = "google-drive")]
365impl From<google_drive3::Error> for Error {
366    fn from(e: google_drive3::Error) -> Self {
367        Error::Internal(anyhow::Error::msg(e.to_string()))
368    }
369}
370
371#[cfg(feature = "google-drive")]
372impl From<google_drive3::hyper::Error> for Error {
373    fn from(e: google_drive3::hyper::Error) -> Self {
374        Error::Internal(e.into())
375    }
376}
377
378pub trait ContextExt<T> {
379    fn context<C: Into<String>>(self, context: C) -> Result<T>;
380    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T>;
381}
382
383impl<T> ContextExt<T> for Result<T> {
384    fn context<C: Into<String>>(self, context: C) -> Result<T> {
385        self.map_err(|e| e.context(context))
386    }
387
388    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
389        self.map_err(|e| e.with_context(f))
390    }
391}
392
393pub trait StdContextExt<T, E> {
394    fn context<C: Into<String>>(self, context: C) -> Result<T>;
395    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T>;
396}
397
398impl<T, E: StdError + Send + Sync + 'static> StdContextExt<T, E> for Result<T, E> {
399    fn context<C: Into<String>>(self, context: C) -> Result<T> {
400        self.map_err(|e| Error::internal(e).context(context))
401    }
402
403    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
404        self.map_err(|e| Error::internal(e).with_context(f))
405    }
406}
407
408impl<T> ContextExt<T> for Option<T> {
409    fn context<C: Into<String>>(self, context: C) -> Result<T> {
410        self.ok_or_else(|| Error::client(context))
411    }
412
413    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
414        self.ok_or_else(|| Error::client(f()))
415    }
416}
417
418impl IntoResponse for Error {
419    fn into_response(self) -> Response {
420        tracing::debug!("Error response:\n{:?}", self);
421
422        let (status_code, error_msg) = match &self {
423            Error::Client { msg, .. } => (StatusCode::BAD_REQUEST, msg.clone()),
424            Error::HostLang(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
425            Error::Context { .. } | Error::Internal(_) => {
426                (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self))
427            }
428        };
429
430        let error_response = ErrorResponse { error: error_msg };
431        (status_code, Json(error_response)).into_response()
432    }
433}
434
435#[macro_export]
436macro_rules! client_bail {
437    ( $fmt:literal $(, $($arg:tt)*)?) => {
438        return Err($crate::error::Error::client(format!($fmt $(, $($arg)*)?)))
439    };
440}
441
442#[macro_export]
443macro_rules! client_error {
444    ( $fmt:literal $(, $($arg:tt)*)?) => {
445        $crate::error::Error::client(format!($fmt $(, $($arg)*)?))
446    };
447}
448
449#[macro_export]
450macro_rules! internal_bail {
451    ( $fmt:literal $(, $($arg:tt)*)?) => {
452        return Err($crate::error::Error::internal_msg(format!($fmt $(, $($arg)*)?)))
453    };
454}
455
456#[macro_export]
457macro_rules! internal_error {
458    ( $fmt:literal $(, $($arg:tt)*)?) => {
459        $crate::error::Error::internal_msg(format!($fmt $(, $($arg)*)?))
460    };
461}
462
463// A wrapper around Error that fits into std::error::Error trait.
464pub struct SError(Error);
465
466impl SError {
467    pub fn inner(&self) -> &Error {
468        &self.0
469    }
470}
471
472impl Display for SError {
473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474        Display::fmt(&self.0, f)
475    }
476}
477
478impl Debug for SError {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        Debug::fmt(&self.0, f)
481    }
482}
483
484impl std::error::Error for SError {
485    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
486        self.0.source()
487    }
488}
489
490// Legacy types below - kept for backwards compatibility during migration
491
492struct ResidualErrorData {
493    message: String,
494    debug: String,
495}
496
497#[derive(Clone)]
498pub struct ResidualError(Arc<ResidualErrorData>);
499
500impl ResidualError {
501    pub fn new<Err: Display + Debug>(err: &Err) -> Self {
502        Self(Arc::new(ResidualErrorData {
503            message: err.to_string(),
504            debug: err.to_string(),
505        }))
506    }
507}
508
509impl Display for ResidualError {
510    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
511        write!(f, "{}", self.0.message)
512    }
513}
514
515impl Debug for ResidualError {
516    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
517        write!(f, "{}", self.0.debug)
518    }
519}
520
521impl StdError for ResidualError {}
522
523enum SharedErrorState {
524    Error(Error),
525    ResidualErrorMessage(ResidualError),
526}
527
528#[derive(Clone)]
529pub struct SharedError(Arc<Mutex<SharedErrorState>>);
530
531impl SharedError {
532    pub fn new(err: Error) -> Self {
533        Self(Arc::new(Mutex::new(SharedErrorState::Error(err))))
534    }
535
536    fn extract_error(&self) -> Error {
537        let mut state = self.0.lock().unwrap();
538        let mut_state = &mut *state;
539
540        let residual_err = match mut_state {
541            SharedErrorState::ResidualErrorMessage(err) => {
542                // Already extracted; return a generic internal error with the residual message.
543                return Error::internal(err.clone());
544            }
545            SharedErrorState::Error(err) => ResidualError::new(err),
546        };
547
548        let orig_state = std::mem::replace(
549            mut_state,
550            SharedErrorState::ResidualErrorMessage(residual_err),
551        );
552        let SharedErrorState::Error(err) = orig_state else {
553            panic!("Expected shared error state to hold Error");
554        };
555        err
556    }
557}
558
559impl Debug for SharedError {
560    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
561        let state = self.0.lock().unwrap();
562        match &*state {
563            SharedErrorState::Error(err) => Debug::fmt(err, f),
564            SharedErrorState::ResidualErrorMessage(err) => Debug::fmt(err, f),
565        }
566    }
567}
568
569impl Display for SharedError {
570    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
571        let state = self.0.lock().unwrap();
572        match &*state {
573            SharedErrorState::Error(err) => Display::fmt(err, f),
574            SharedErrorState::ResidualErrorMessage(err) => Display::fmt(err, f),
575        }
576    }
577}
578
579impl From<Error> for SharedError {
580    fn from(err: Error) -> Self {
581        Self(Arc::new(Mutex::new(SharedErrorState::Error(err))))
582    }
583}
584
585pub fn shared_ok<T>(value: T) -> std::result::Result<T, SharedError> {
586    Ok(value)
587}
588
589pub type SharedResult<T> = std::result::Result<T, SharedError>;
590
591pub trait SharedResultExt<T> {
592    fn into_result(self) -> Result<T>;
593}
594
595impl<T> SharedResultExt<T> for std::result::Result<T, SharedError> {
596    fn into_result(self) -> Result<T> {
597        match self {
598            Ok(value) => Ok(value),
599            Err(err) => Err(err.extract_error()),
600        }
601    }
602}
603
604pub trait SharedResultExtRef<'a, T> {
605    fn into_result(self) -> Result<&'a T>;
606}
607
608impl<'a, T> SharedResultExtRef<'a, T> for &'a std::result::Result<T, SharedError> {
609    fn into_result(self) -> Result<&'a T> {
610        match self {
611            Ok(value) => Ok(value),
612            Err(err) => Err(err.extract_error()),
613        }
614    }
615}
616
617pub fn invariance_violation() -> anyhow::Error {
618    anyhow::anyhow!("Invariance violation")
619}
620
621#[derive(Debug)]
622pub struct ApiError {
623    pub err: anyhow::Error,
624    pub status_code: StatusCode,
625}
626
627impl ApiError {
628    pub fn new(message: &str, status_code: StatusCode) -> Self {
629        Self {
630            err: anyhow::anyhow!("{}", message),
631            status_code,
632        }
633    }
634}
635
636impl Display for ApiError {
637    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
638        Display::fmt(&self.err, f)
639    }
640}
641
642impl StdError for ApiError {
643    fn source(&self) -> Option<&(dyn StdError + 'static)> {
644        self.err.source()
645    }
646}
647
648#[derive(Serialize)]
649struct ErrorResponse {
650    error: String,
651}
652
653impl IntoResponse for ApiError {
654    fn into_response(self) -> Response {
655        tracing::debug!("Internal server error:\n{:?}", self.err);
656        let error_response = ErrorResponse {
657            error: format!("{:?}", self.err),
658        };
659        (self.status_code, Json(error_response)).into_response()
660    }
661}
662
663impl From<anyhow::Error> for ApiError {
664    fn from(err: anyhow::Error) -> ApiError {
665        if err.is::<ApiError>() {
666            return err.downcast::<ApiError>().unwrap();
667        }
668        Self {
669            err,
670            status_code: StatusCode::INTERNAL_SERVER_ERROR,
671        }
672    }
673}
674
675impl From<Error> for ApiError {
676    fn from(err: Error) -> ApiError {
677        let status_code = match err.without_contexts() {
678            Error::Client { .. } => StatusCode::BAD_REQUEST,
679            _ => StatusCode::INTERNAL_SERVER_ERROR,
680        };
681        ApiError {
682            err: anyhow::Error::from(err.std_error()),
683            status_code,
684        }
685    }
686}
687
688#[macro_export]
689macro_rules! api_bail {
690    ( $fmt:literal $(, $($arg:tt)*)?) => {
691        return Err($crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?), axum::http::StatusCode::BAD_REQUEST).into())
692    };
693}
694
695#[macro_export]
696macro_rules! api_error {
697    ( $fmt:literal $(, $($arg:tt)*)?) => {
698        $crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?), axum::http::StatusCode::BAD_REQUEST)
699    };
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705    use std::backtrace::BacktraceStatus;
706    use std::io;
707
708    #[derive(Debug)]
709    struct MockHostError(String);
710
711    impl Display for MockHostError {
712        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
713            write!(f, "MockHostError: {}", self.0)
714        }
715    }
716
717    impl StdError for MockHostError {}
718
719    #[test]
720    fn test_client_error_creation() {
721        let err = Error::client("invalid input");
722        assert!(matches!(&err, Error::Client { msg, .. } if msg == "invalid input"));
723        assert!(matches!(err.without_contexts(), Error::Client { .. }));
724    }
725
726    #[test]
727    fn test_internal_error_creation() {
728        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
729        let err: Error = io_err.into();
730        assert!(matches!(err, Error::Internal { .. }));
731    }
732
733    #[test]
734    fn test_internal_msg_error_creation() {
735        let err = Error::internal_msg("something went wrong");
736        assert!(matches!(err, Error::Internal { .. }));
737        assert_eq!(err.to_string(), "something went wrong");
738    }
739
740    #[test]
741    fn test_host_error_creation_and_detection() {
742        let mock = MockHostError("test error".to_string());
743        let err = Error::host(mock);
744        assert!(matches!(err.without_contexts(), Error::HostLang(_)));
745
746        if let Error::HostLang(host_err) = err.without_contexts() {
747            let any: &dyn Any = host_err.as_ref();
748            let downcasted = any.downcast_ref::<MockHostError>();
749            assert!(downcasted.is_some());
750            assert_eq!(downcasted.unwrap().0, "test error");
751        } else {
752            panic!("Expected HostLang variant");
753        }
754    }
755
756    #[test]
757    fn test_context_chaining() {
758        let inner = Error::client("base error");
759        let with_context: Result<()> = Err(inner);
760        let wrapped = ContextExt::context(
761            ContextExt::context(ContextExt::context(with_context, "layer 1"), "layer 2"),
762            "layer 3",
763        );
764
765        let err = wrapped.unwrap_err();
766        assert!(matches!(&err, Error::Context { msg, .. } if msg == "layer 3"));
767
768        if let Error::Context { source, .. } = &err {
769            assert!(
770                matches!(source.as_ref(), SError(Error::Context { msg, .. }) if msg == "layer 2")
771            );
772        }
773        assert_eq!(
774            err.to_string(),
775            "\nContext:\
776             \n  1: layer 3\
777             \n  2: layer 2\
778             \n  3: layer 1\
779             \nInvalid Request: base error"
780        );
781    }
782
783    #[test]
784    fn test_context_preserves_host_error() {
785        let mock = MockHostError("original python error".to_string());
786        let err = Error::host(mock);
787        let wrapped: Result<()> = Err(err);
788        let with_context = ContextExt::context(wrapped, "while processing request");
789
790        let final_err = with_context.unwrap_err();
791        assert!(matches!(final_err.without_contexts(), Error::HostLang(_)));
792
793        if let Error::HostLang(host_err) = final_err.without_contexts() {
794            let any: &dyn Any = host_err.as_ref();
795            let downcasted = any.downcast_ref::<MockHostError>();
796            assert!(downcasted.is_some());
797            assert_eq!(downcasted.unwrap().0, "original python error");
798        } else {
799            panic!("Expected HostLang variant");
800        }
801    }
802
803    #[test]
804    fn test_backtrace_captured_for_client_error() {
805        let err = Error::client("test");
806        let bt = err.backtrace();
807        assert!(bt.is_some());
808        let status = bt.unwrap().status();
809        assert!(
810            status == BacktraceStatus::Captured
811                || status == BacktraceStatus::Disabled
812                || status == BacktraceStatus::Unsupported
813        );
814    }
815
816    #[test]
817    fn test_backtrace_captured_for_internal_error() {
818        let err = Error::internal_msg("test internal");
819        let bt = err.backtrace();
820        assert!(bt.is_some());
821    }
822
823    #[test]
824    fn test_backtrace_traverses_context() {
825        let inner = Error::internal_msg("base");
826        let wrapped: Result<()> = Err(inner);
827        let with_context = ContextExt::context(wrapped, "context");
828
829        let err = with_context.unwrap_err();
830        let bt = err.backtrace();
831        assert!(bt.is_some());
832    }
833
834    #[test]
835    fn test_option_context_ext() {
836        let opt: Option<i32> = None;
837        let result = opt.context("value was missing");
838
839        assert!(result.is_err());
840        let err = result.unwrap_err();
841        assert!(matches!(err.without_contexts(), Error::Client { .. }));
842        assert!(matches!(&err, Error::Client { msg, .. } if msg == "value was missing"));
843    }
844
845    #[test]
846    fn test_error_display_formats() {
847        let client_err = Error::client("bad input");
848        assert_eq!(client_err.to_string(), "Invalid Request: bad input");
849
850        let internal_err = Error::internal_msg("db connection failed");
851        assert_eq!(internal_err.to_string(), "db connection failed");
852
853        let host_err = Error::host(MockHostError("py error".to_string()));
854        assert_eq!(host_err.to_string(), "MockHostError: py error");
855    }
856
857    #[test]
858    fn test_error_source_chain() {
859        let inner = Error::internal_msg("root cause");
860        let wrapped: Result<()> = Err(inner);
861        let outer = ContextExt::context(wrapped, "outer context").unwrap_err();
862
863        let source = outer.source();
864        assert!(source.is_some());
865    }
866}