Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.20.0] - 2025-09-30

### Added
- Added a `Context::redact_field_mut` builder method to tweak metadata
redaction policies in place before attaching additional fields.
- Extended response tests to cover JSON serialization of redacted payloads and
hashed metadata along with checks for the opt-in internal formatters.

### Changed
- Verified `ErrorResponse` and `ProblemJson` serialization respects message and
metadata redaction policies, ensuring secrets stay out of wire payloads while
keeping diagnostic logging intact.

## [0.19.0] - 2025-09-29

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.19.0"
version = "0.20.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes.

~~~toml
[dependencies]
masterror = { version = "0.19.0", default-features = false }
masterror = { version = "0.20.0", default-features = false }
# or with features:
# masterror = { version = "0.19.0", features = [
# masterror = { version = "0.20.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
Expand Down Expand Up @@ -78,10 +78,10 @@ masterror = { version = "0.19.0", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.19.0", default-features = false }
masterror = { version = "0.20.0", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.19.0", features = [
# masterror = { version = "0.20.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
Expand Down Expand Up @@ -720,13 +720,13 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
Minimal core:

~~~toml
masterror = { version = "0.19.0", default-features = false }
masterror = { version = "0.20.0", default-features = false }
~~~

API (Axum + JSON + deps):

~~~toml
masterror = { version = "0.19.0", features = [
masterror = { version = "0.20.0", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand All @@ -735,7 +735,7 @@ masterror = { version = "0.19.0", features = [
API (Actix + JSON + deps):

~~~toml
masterror = { version = "0.19.0", features = [
masterror = { version = "0.20.0", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand Down
31 changes: 23 additions & 8 deletions src/app_error/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,18 @@ impl Context {
/// Override the redaction policy for a metadata field.
#[must_use]
pub fn redact_field(mut self, name: &'static str, redaction: FieldRedaction) -> Self {
self.field_policies
.retain(|(existing, _)| *existing != name);
self.field_policies.push((name, redaction));
for field in &mut self.fields {
if field.name() == name {
field.set_redaction(redaction);
}
}
self.set_field_policy(name, redaction);
self
}

/// Override the redaction policy for a metadata field in place.
#[must_use]
pub fn redact_field_mut(
&mut self,
name: &'static str,
redaction: FieldRedaction
) -> &mut Self {
self.set_field_policy(name, redaction);
self
}

Expand Down Expand Up @@ -180,4 +184,15 @@ impl Context {
}
}
}

fn set_field_policy(&mut self, name: &'static str, redaction: FieldRedaction) {
self.field_policies
.retain(|(existing, _)| *existing != name);
self.field_policies.push((name, redaction));
for field in &mut self.fields {
if field.name() == name {
field.set_redaction(redaction);
}
}
}
}
15 changes: 15 additions & 0 deletions src/app_error/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,21 @@ fn context_redact_field_overrides_policy() {
assert_eq!(metadata.redaction("token"), Some(FieldRedaction::Redact));
}

#[test]
fn context_redact_field_mut_applies_policies() {
let mut context = super::Context::new(AppErrorKind::Service);
let _ = context.redact_field_mut("token", FieldRedaction::Hash);
context = context.with(field::str("token", "super-secret"));

let err = context.into_error(DummyError);
let metadata = err.metadata();
assert_eq!(
metadata.get("token"),
Some(&FieldValue::Str(Cow::Borrowed("super-secret")))
);
assert_eq!(metadata.redaction("token"), Some(FieldRedaction::Hash));
}

#[test]
fn app_error_redact_field_updates_metadata() {
let err = AppError::internal("boom")
Expand Down
55 changes: 55 additions & 0 deletions src/response/problem_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,11 @@ pub fn mapping_for_code(code: AppCode) -> CodeMapping {

#[cfg(test)]
mod tests {
use std::fmt::Write;

use serde_json::Value;
use sha2::{Digest, Sha256};

use super::*;
use crate::AppError;

Expand Down Expand Up @@ -909,6 +914,56 @@ mod tests {
}
}

#[test]
fn problem_json_serialization_masks_sensitive_metadata() {
let secret = "super-secret";
let err = AppError::internal("oops").with_field(crate::field::str("token", secret));
let problem = ProblemJson::from_ref(&err);
let json = serde_json::to_value(&problem).expect("serialize problem");

let metadata = json
.get("metadata")
.and_then(Value::as_object)
.expect("metadata present");
let hashed = metadata
.get("token")
.and_then(Value::as_str)
.expect("hashed token");

let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
let digest = hasher.finalize();
let expected = digest
.iter()
.fold(String::with_capacity(64), |mut acc, byte| {
let _ = write!(&mut acc, "{:02x}", byte);
acc
});

assert_eq!(hashed, expected);
assert!(!json.to_string().contains(secret));

let debug_repr = format!("{:?}", problem.internal());
assert!(debug_repr.contains("metadata"));
assert!(!debug_repr.contains(secret));
}

#[test]
fn problem_json_serialization_omits_metadata_when_redacted() {
let secret_value = "sensitive-value";
let err = AppError::internal("secret")
.redactable()
.with_field(crate::field::str("token", secret_value));
let problem = ProblemJson::from_ref(&err);
let json = serde_json::to_value(&problem).expect("serialize problem");

assert!(json.get("metadata").is_none());
assert!(!json.to_string().contains(secret_value));

let debug_repr = format!("{:?}", problem.internal());
assert!(debug_repr.contains("ProblemJson"));
}

#[test]
fn mapping_for_every_code_matches_http_status() {
for (code, mapping) in CODE_MAPPINGS {
Expand Down
14 changes: 14 additions & 0 deletions src/response/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ fn from_app_error_redacts_message_when_policy_allows() {
assert_eq!(resp_ref.message, AppErrorKind::Internal.to_string());
}

#[test]
fn error_response_serialization_hides_redacted_message() {
let secret = "super-secret";
let resp: ErrorResponse = AppError::internal(secret).redactable().into();
let json = serde_json::to_value(&resp).expect("serialize response");

let fallback = AppErrorKind::Internal.to_string();
assert_eq!(
json.get("message").and_then(|value| value.as_str()),
Some(fallback.as_str())
);
assert!(!json.to_string().contains(secret));
}

// --- Display formatting --------------------------------------------------

#[test]
Expand Down
Loading