diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e96b5..19f8d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.24.3] - 2025-10-19 + +### Fixed +- Reused stack-allocated format buffers when emitting gRPC metadata for HTTP + status codes and retry hints, and added regression coverage to ensure metadata + strings remain ASCII encoded. + ## [0.24.2] - 2025-10-18 ### Added diff --git a/Cargo.lock b/Cargo.lock index 9a2c043..9e4de6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1826,7 +1826,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.24.2" +version = "0.24.3" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index ca7a6c8..7965c7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.24.2" +version = "0.24.3" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 8a5ca8c..c9279af 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.2", default-features = false } +masterror = { version = "0.24.3", default-features = false } # or with features: -# masterror = { version = "0.24.2", features = [ +# masterror = { version = "0.24.3", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "sqlx", "sqlx-migrate", "reqwest", "redis", diff --git a/src/convert/tonic.rs b/src/convert/tonic.rs index a7bb30e..5302514 100644 --- a/src/convert/tonic.rs +++ b/src/convert/tonic.rs @@ -21,6 +21,7 @@ use core::convert::Infallible; use std::borrow::Cow; +use itoa::Buffer as IntegerBuffer; use tonic::{ Code, Status, metadata::{MetadataMap, MetadataValue} @@ -68,11 +69,9 @@ fn status_from_error(error: &Error) -> Status { let mut meta = MetadataMap::new(); insert_ascii(&mut meta, "app-code", error.code.as_str()); - insert_ascii( - &mut meta, - "app-http-status", - mapping.http_status().to_string() - ); + let mut http_status_buffer = IntegerBuffer::new(); + let http_status = http_status_buffer.format(mapping.http_status()); + insert_ascii(&mut meta, "app-http-status", http_status); insert_ascii(&mut meta, "app-problem-type", mapping.problem_type()); if let Some(advice) = error.retry { @@ -104,7 +103,9 @@ fn sanitize_detail( } fn insert_retry(meta: &mut MetadataMap, retry: RetryAdvice) { - insert_ascii(meta, "retry-after", retry.after_seconds.to_string()); + let mut retry_after_buffer = IntegerBuffer::new(); + let retry_after = retry_after_buffer.format(retry.after_seconds); + insert_ascii(meta, "retry-after", retry_after); } fn attach_metadata(meta: &mut MetadataMap, metadata: &Metadata) { @@ -215,4 +216,22 @@ mod tests { Some("2") ); } + + #[test] + fn timeout_status_carries_ascii_metadata() { + let status = Status::from(AppError::timeout("deadline exceeded").with_retry_after_secs(7)); + let metadata = status.metadata(); + assert_eq!( + metadata + .get("app-http-status") + .and_then(|value| value.to_str().ok()), + Some("504") + ); + assert_eq!( + metadata + .get("retry-after") + .and_then(|value| value.to_str().ok()), + Some("7") + ); + } }