diff --git a/CHANGELOG.md b/CHANGELOG.md index cf60a98..1dd5fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.24.17] - 2025-11-02 + +### Fixed +- Preserve captured backtraces when wrapping `AppError` instances through + `ResultExt::context` by sharing the snapshot instead of attempting to clone + `std::backtrace::Backtrace`. + ## [0.24.16] - 2025-11-01 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index a41ebf0..bfff7a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1804,7 +1804,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.24.16" +version = "0.24.17" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 5860848..1004cda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "masterror" -version = "0.24.16" +version = "0.24.17" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index ff07fea..520cb40 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,9 @@ The build script keeps the full feature snippet below in sync with ~~~toml [dependencies] -masterror = { version = "0.24.16", default-features = false } +masterror = { version = "0.24.17", default-features = false } # or with features: -# masterror = { version = "0.24.16", features = [ +# masterror = { version = "0.24.17", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "sqlx", "sqlx-migrate", "reqwest", "redis", diff --git a/src/app_error/core.rs b/src/app_error/core.rs index 26e1168..7ccf280 100644 --- a/src/app_error/core.rs +++ b/src/app_error/core.rs @@ -91,9 +91,9 @@ pub struct ErrorInner { pub details: Option, pub source: Option>, #[cfg(feature = "backtrace")] - pub backtrace: Option, + pub backtrace: Option>, #[cfg(feature = "backtrace")] - pub captured_backtrace: OnceLock>, + pub captured_backtrace: OnceLock>>, telemetry_dirty: AtomicBool, #[cfg(feature = "tracing")] tracing_dirty: AtomicBool @@ -110,9 +110,9 @@ const BACKTRACE_STATE_DISABLED: u8 = 2; static BACKTRACE_STATE: AtomicU8 = AtomicU8::new(BACKTRACE_STATE_UNSET); #[cfg(feature = "backtrace")] -fn capture_backtrace_snapshot() -> Option { +fn capture_backtrace_snapshot() -> Option> { if should_capture_backtrace() { - Some(Backtrace::capture()) + Some(Arc::new(Backtrace::capture())) } else { None } @@ -321,13 +321,13 @@ impl Error { #[cfg(feature = "backtrace")] fn capture_backtrace(&self) -> Option<&CapturedBacktrace> { - if let Some(backtrace) = self.backtrace.as_ref() { + if let Some(backtrace) = self.backtrace.as_deref() { return Some(backtrace); } self.captured_backtrace .get_or_init(capture_backtrace_snapshot) - .as_ref() + .as_deref() } #[cfg(not(feature = "backtrace"))] @@ -336,7 +336,7 @@ impl Error { } #[cfg(feature = "backtrace")] - fn set_backtrace_slot(&mut self, backtrace: CapturedBacktrace) { + fn set_backtrace_slot(&mut self, backtrace: Arc) { self.backtrace = Some(backtrace); self.captured_backtrace = OnceLock::new(); } @@ -574,6 +574,21 @@ impl Error { /// Attach a captured backtrace. #[must_use] pub fn with_backtrace(mut self, backtrace: CapturedBacktrace) -> Self { + #[cfg(feature = "backtrace")] + { + self.set_backtrace_slot(Arc::new(backtrace)); + } + + #[cfg(not(feature = "backtrace"))] + { + self.set_backtrace_slot(backtrace); + } + self.mark_dirty(); + self + } + + #[cfg(feature = "backtrace")] + pub(crate) fn with_shared_backtrace(mut self, backtrace: Arc) -> Self { self.set_backtrace_slot(backtrace); self.mark_dirty(); self @@ -678,6 +693,18 @@ impl Error { self.capture_backtrace() } + #[cfg(feature = "backtrace")] + pub(crate) fn backtrace_shared(&self) -> Option> { + if let Some(backtrace) = self.backtrace.as_ref() { + return Some(Arc::clone(backtrace)); + } + + self.captured_backtrace + .get_or_init(capture_backtrace_snapshot) + .as_ref() + .map(Arc::clone) + } + /// Borrow the source if present. #[must_use] pub fn source_ref(&self) -> Option<&(dyn CoreError + Send + Sync + 'static)> { diff --git a/src/result_ext.rs b/src/result_ext.rs index 4b46b9a..790a095 100644 --- a/src/result_ext.rs +++ b/src/result_ext.rs @@ -102,8 +102,11 @@ impl ResultExt for Result { enriched.details = app_err.details.clone(); } #[cfg(feature = "backtrace")] - if let Some(backtrace) = app_err.backtrace().cloned() { - enriched = enriched.with_backtrace(backtrace); + let shared_backtrace = app_err.backtrace_shared(); + + #[cfg(feature = "backtrace")] + if let Some(backtrace) = shared_backtrace { + enriched = enriched.with_shared_backtrace(backtrace); } enriched.with_context(app_err)