diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b5dad..cf9c07f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.24.1] - 2025-10-17 + +### Fixed +- Updated `Context::into_error` to move dynamic `AppCode` values into the + resulting `AppError`, reworking field redaction plumbing to avoid clones and + preserve custom code ownership. Added a regression test covering pointer + identity for context-promoted errors. + ## [0.24.0] - 2025-10-16 ### Added diff --git a/Cargo.lock b/Cargo.lock index 5102aca..e4f108c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1665,7 +1665,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.24.0" +version = "0.24.1" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index e28329d..801b6f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.24.0" +version = "0.24.1" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 855543d..dcfe183 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,9 @@ The build script keeps the full feature snippet below in sync with ~~~toml [dependencies] -masterror = { version = "0.24.0", default-features = false } +masterror = { version = "0.24.1", default-features = false } # or with features: -# masterror = { version = "0.24.0", features = [ +# masterror = { version = "0.24.1", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "sqlx", "sqlx-migrate", "reqwest", "redis", diff --git a/src/app_error/context.rs b/src/app_error/context.rs index 5a5fcb3..38e75fd 100644 --- a/src/app_error/context.rs +++ b/src/app_error/context.rs @@ -91,7 +91,7 @@ impl Context { #[must_use] pub fn with(mut self, field: Field) -> Self { self.fields.push(field); - self.apply_field_redactions(); + Self::apply_field_redactions(&mut self.fields, &self.field_policies); self } @@ -132,35 +132,45 @@ impl Context { self } - pub(crate) fn into_error(mut self, source: E) -> Error + pub(crate) fn into_error(self, source: E) -> Error where E: CoreError + Send + Sync + 'static { - if let Some(location) = self.caller_location { - self.fields.push(Field::new( + let Context { + mut fields, + field_policies, + edit_policy, + caller_location, + code, + category, + .. + } = self; + + if let Some(location) = caller_location { + fields.push(Field::new( "caller.file", FieldValue::Str(location.file().into()) )); - self.fields.push(Field::new( + fields.push(Field::new( "caller.line", FieldValue::U64(u64::from(location.line())) )); - self.fields.push(Field::new( + fields.push(Field::new( "caller.column", FieldValue::U64(u64::from(location.column())) )); } - let mut error = AppError::new_raw(self.category, None); - error.code = self.code.clone(); - if !self.fields.is_empty() { - self.apply_field_redactions(); - error.metadata.extend(self.fields); + let mut error = AppError::new_raw(category, None); + error.code = code; + if !fields.is_empty() { + Self::apply_field_redactions(&mut fields, &field_policies); + error.metadata.extend(fields); } - for &(name, redaction) in &self.field_policies { + for &(name, redaction) in &field_policies { error = error.redact_field(name, redaction); } - if matches!(self.edit_policy, MessageEditPolicy::Redact) { + if matches!(edit_policy, MessageEditPolicy::Redact) { error.edit_policy = MessageEditPolicy::Redact; } let error = error.with_context(source); @@ -170,13 +180,15 @@ impl Context { } impl Context { - fn apply_field_redactions(&mut self) { - if self.field_policies.is_empty() { + fn apply_field_redactions( + fields: &mut Vec, + policies: &[(&'static str, FieldRedaction)] + ) { + if policies.is_empty() { return; } - for field in &mut self.fields { - if let Some((_, policy)) = self - .field_policies + for field in fields { + if let Some((_, policy)) = policies .iter() .rev() .find(|(name, _)| *name == field.name()) diff --git a/src/app_error/tests.rs b/src/app_error/tests.rs index 4c54786..6f0b0da 100644 --- a/src/app_error/tests.rs +++ b/src/app_error/tests.rs @@ -39,7 +39,7 @@ static BACKTRACE_ENV_GUARD: Mutex<()> = Mutex::new(()); static TELEMETRY_GUARD: Mutex<()> = Mutex::new(()); use super::{AppError, FieldRedaction, FieldValue, MessageEditPolicy, field}; -use crate::{AppCode, AppErrorKind, ErrorResponse}; +use crate::{AppCode, AppErrorKind, Context, ErrorResponse, ResultExt}; // --- Helpers ------------------------------------------------------------- @@ -198,6 +198,19 @@ fn retry_and_www_authenticate_are_attached() { assert_eq!(err.www_authenticate.as_deref(), Some("Bearer")); } +#[test] +fn context_moves_dynamic_code_without_cloning() { + let dynamic_code = + AppCode::try_new(String::from("THIRD_PARTY_FAILURE")).expect("valid dynamic code"); + let expected_ptr = dynamic_code.as_str().as_ptr(); + + let err = Result::<(), IoError>::Err(IoError::from(IoErrorKind::Other)) + .ctx(|| Context::new(AppErrorKind::Service).code(dynamic_code)) + .unwrap_err(); + + assert_eq!(err.code.as_str().as_ptr(), expected_ptr); +} + #[test] fn render_message_does_not_allocate_for_borrowed_str() { let err = AppError::new(AppErrorKind::BadRequest, "borrowed"); diff --git a/tests/ui/app_error/fail/enum_missing_variant.stderr b/tests/ui/app_error/fail/enum_missing_variant.stderr index bbc297c..d000de1 100644 --- a/tests/ui/app_error/fail/enum_missing_variant.stderr +++ b/tests/ui/app_error/fail/enum_missing_variant.stderr @@ -1,9 +1,8 @@ error: all variants must use #[app_error(...)] to derive AppError conversion --> tests/ui/app_error/fail/enum_missing_variant.rs:8:5 | -8 | / #[error("without")] -9 | | Without, - | |___________^ +8 | #[error("without")] + | ^ warning: unused import: `AppErrorKind` --> tests/ui/app_error/fail/enum_missing_variant.rs:1:17 @@ -11,4 +10,4 @@ warning: unused import: `AppErrorKind` 1 | use masterror::{AppErrorKind, Error}; | ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/app_error/fail/missing_code.stderr b/tests/ui/app_error/fail/missing_code.stderr index 4f02301..70ccade 100644 --- a/tests/ui/app_error/fail/missing_code.stderr +++ b/tests/ui/app_error/fail/missing_code.stderr @@ -2,7 +2,7 @@ error: AppCode conversion requires `code = ...` in #[app_error(...)] --> tests/ui/app_error/fail/missing_code.rs:9:5 | 9 | #[app_error(kind = AppErrorKind::Service)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^ warning: unused imports: `AppCode` and `AppErrorKind` --> tests/ui/app_error/fail/missing_code.rs:1:17 @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Error}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/app_error/fail/missing_kind.stderr b/tests/ui/app_error/fail/missing_kind.stderr index 021c135..c615e98 100644 --- a/tests/ui/app_error/fail/missing_kind.stderr +++ b/tests/ui/app_error/fail/missing_kind.stderr @@ -2,4 +2,4 @@ error: missing `kind = ...` in #[app_error(...)] --> tests/ui/app_error/fail/missing_kind.rs:5:1 | 5 | #[app_error(message)] - | ^^^^^^^^^^^^^^^^^^^^^ + | ^ diff --git a/tests/ui/formatter/fail/duplicate_fmt.stderr b/tests/ui/formatter/fail/duplicate_fmt.stderr index 5b8f363..5b08225 100644 --- a/tests/ui/formatter/fail/duplicate_fmt.stderr +++ b/tests/ui/formatter/fail/duplicate_fmt.stderr @@ -2,4 +2,4 @@ error: duplicate `fmt` handler specified --> tests/ui/formatter/fail/duplicate_fmt.rs:4:36 | 4 | #[error(fmt = crate::format_error, fmt = crate::format_error)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^^ diff --git a/tests/ui/formatter/fail/implicit_after_named.stderr b/tests/ui/formatter/fail/implicit_after_named.stderr index be76742..d416399 100644 --- a/tests/ui/formatter/fail/implicit_after_named.stderr +++ b/tests/ui/formatter/fail/implicit_after_named.stderr @@ -8,5 +8,4 @@ error: multiple unused formatting arguments | argument never used | argument never used | - = note: consider adding 2 format specifiers = note: this error originates in the derive macro `Error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/formatter/fail/unsupported_flag.stderr b/tests/ui/formatter/fail/unsupported_flag.stderr index b8bf229..d7acdb1 100644 --- a/tests/ui/formatter/fail/unsupported_flag.stderr +++ b/tests/ui/formatter/fail/unsupported_flag.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..11 uses an unsupported formatter - --> tests/ui/formatter/fail/unsupported_flag.rs:4:10 + --> tests/ui/formatter/fail/unsupported_flag.rs:4:9 | 4 | #[error("{value:##x}")] - | ^^^^^^^^^^^ + | ^^^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/unsupported_formatter.stderr b/tests/ui/formatter/fail/unsupported_formatter.stderr index a6a40c2..5869420 100644 --- a/tests/ui/formatter/fail/unsupported_formatter.stderr +++ b/tests/ui/formatter/fail/unsupported_formatter.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..9 uses an unsupported formatter - --> tests/ui/formatter/fail/unsupported_formatter.rs:4:10 + --> tests/ui/formatter/fail/unsupported_formatter.rs:4:9 | 4 | #[error("{value:y}")] - | ^^^^^^^^^ + | ^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/uppercase_binary.stderr b/tests/ui/formatter/fail/uppercase_binary.stderr index 3d332c7..bbe04b4 100644 --- a/tests/ui/formatter/fail/uppercase_binary.stderr +++ b/tests/ui/formatter/fail/uppercase_binary.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..9 uses an unsupported formatter - --> tests/ui/formatter/fail/uppercase_binary.rs:4:10 + --> tests/ui/formatter/fail/uppercase_binary.rs:4:9 | 4 | #[error("{value:B}")] - | ^^^^^^^^^ + | ^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/uppercase_pointer.stderr b/tests/ui/formatter/fail/uppercase_pointer.stderr index 0bd10fa..2c30e71 100644 --- a/tests/ui/formatter/fail/uppercase_pointer.stderr +++ b/tests/ui/formatter/fail/uppercase_pointer.stderr @@ -1,5 +1,5 @@ error: placeholder spanning bytes 0..9 uses an unsupported formatter - --> tests/ui/formatter/fail/uppercase_pointer.rs:4:10 + --> tests/ui/formatter/fail/uppercase_pointer.rs:4:9 | 4 | #[error("{value:P}")] - | ^^^^^^^^^ + | ^^^^^^^^^^^ diff --git a/tests/ui/masterror/fail/duplicate_attr.stderr b/tests/ui/masterror/fail/duplicate_attr.stderr index 113a10d..c3fb86b 100644 --- a/tests/ui/masterror/fail/duplicate_attr.stderr +++ b/tests/ui/masterror/fail/duplicate_attr.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/masterror/fail/duplicate_telemetry.stderr b/tests/ui/masterror/fail/duplicate_telemetry.stderr index 9ada290..b331baa 100644 --- a/tests/ui/masterror/fail/duplicate_telemetry.stderr +++ b/tests/ui/masterror/fail/duplicate_telemetry.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/masterror/fail/empty_redact.stderr b/tests/ui/masterror/fail/empty_redact.stderr index fd151cc..b2658a1 100644 --- a/tests/ui/masterror/fail/empty_redact.stderr +++ b/tests/ui/masterror/fail/empty_redact.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/masterror/fail/enum_missing_variant.stderr b/tests/ui/masterror/fail/enum_missing_variant.stderr index 5a25e12..83d517f 100644 --- a/tests/ui/masterror/fail/enum_missing_variant.stderr +++ b/tests/ui/masterror/fail/enum_missing_variant.stderr @@ -1,9 +1,8 @@ error: all variants must use #[masterror(...)] to derive masterror::Error conversion --> tests/ui/masterror/fail/enum_missing_variant.rs:8:5 | -8 | / #[error("missing")] -9 | | Missing - | |___________^ +8 | #[error("missing")] + | ^ warning: unused imports: `AppCode` and `AppErrorKind` --> tests/ui/masterror/fail/enum_missing_variant.rs:1:17 @@ -11,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/masterror/fail/missing_category.stderr b/tests/ui/masterror/fail/missing_category.stderr index bdadf45..f929951 100644 --- a/tests/ui/masterror/fail/missing_category.stderr +++ b/tests/ui/masterror/fail/missing_category.stderr @@ -2,7 +2,7 @@ error: missing `category = ...` in #[masterror(...)] --> tests/ui/masterror/fail/missing_category.rs:5:1 | 5 | #[masterror(code = AppCode::Internal)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^ warning: unused import: `AppCode` --> tests/ui/masterror/fail/missing_category.rs:1:17 @@ -10,4 +10,4 @@ warning: unused import: `AppCode` 1 | use masterror::{AppCode, Masterror}; | ^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/masterror/fail/missing_code.stderr b/tests/ui/masterror/fail/missing_code.stderr index 037fac8..34abc91 100644 --- a/tests/ui/masterror/fail/missing_code.stderr +++ b/tests/ui/masterror/fail/missing_code.stderr @@ -2,7 +2,7 @@ error: missing `code = ...` in #[masterror(...)] --> tests/ui/masterror/fail/missing_code.rs:5:1 | 5 | #[masterror(category = AppErrorKind::Internal)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^ warning: unused import: `AppErrorKind` --> tests/ui/masterror/fail/missing_code.rs:1:17 @@ -10,4 +10,4 @@ warning: unused import: `AppErrorKind` 1 | use masterror::{AppErrorKind, Masterror}; | ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/masterror/fail/unknown_option.stderr b/tests/ui/masterror/fail/unknown_option.stderr index 1822edf..d579838 100644 --- a/tests/ui/masterror/fail/unknown_option.stderr +++ b/tests/ui/masterror/fail/unknown_option.stderr @@ -10,4 +10,4 @@ warning: unused imports: `AppCode` and `AppErrorKind` 1 | use masterror::{AppCode, AppErrorKind, Masterror}; | ^^^^^^^ ^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default