From eafdcfec88589fcbd3fb3a8069adb984e7b273f6 Mon Sep 17 00:00:00 2001 From: Isvane <277444536+Isvane@users.noreply.github.com> Date: Thu, 21 May 2026 16:50:17 +0700 Subject: [PATCH 1/2] fix(services)!: reject trailing slash for file paths (#678) * fix(services): handle trailing slash * fix(services): handle trailing slashes on file requests * fix(services): return 404 for file requests with trailing slash * test: add test for file request with trailing slash and fallback * fix(services): return 404 for single file requests with trailing slash * revert SingleFile changes * fix(services): correctly handle file requests with trailing slash This fixes the regression while preserving the correct behavior for: - Directories with trailing slash + `append_index_html_on_directories: true` - Root path `/` (with or without trailing slash) - Normal file requests We now explicitly reject file + trailing-slash early in `maybe_redirect_or_append_path` and use a `PathResolution` enum for clearer control flow. * satisfy clippy * remove redundant syscall * revert maybe_redirect_or_append_path to use Option --- test-files/foo/index.html | 1 + .../src/services/fs/serve_dir/open_file.rs | 13 +++- tower-http/src/services/fs/serve_dir/tests.rs | 66 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 test-files/foo/index.html diff --git a/test-files/foo/index.html b/test-files/foo/index.html new file mode 100644 index 00000000..ef2c5cc2 --- /dev/null +++ b/test-files/foo/index.html @@ -0,0 +1 @@ +HTML! diff --git a/tower-http/src/services/fs/serve_dir/open_file.rs b/tower-http/src/services/fs/serve_dir/open_file.rs index 9dd1e012..ff1d7e46 100644 --- a/tower-http/src/services/fs/serve_dir/open_file.rs +++ b/tower-http/src/services/fs/serve_dir/open_file.rs @@ -121,6 +121,7 @@ pub(super) async fn open_file( }; let meta = file.metadata().await?; + let last_modified = meta.modified().ok().map(LastModified::from); if let Some(output) = check_modified_headers( last_modified.as_ref(), @@ -283,7 +284,15 @@ async fn maybe_redirect_or_append_path( uri: &Uri, append_index_html_on_directories: bool, ) -> Option { - if !is_dir(path_to_file).await { + let uri_path = uri.path(); + + let is_directory = is_dir(path_to_file).await; + + if uri_path.ends_with('/') && uri_path != "/" && !is_directory { + return Some(OpenFileOutput::FileNotFound); + } + + if !is_directory { return None; } @@ -291,7 +300,7 @@ async fn maybe_redirect_or_append_path( return Some(OpenFileOutput::FileNotFound); } - if uri.path().ends_with('/') { + if uri_path.ends_with('/') { path_to_file.push("index.html"); None } else { diff --git a/tower-http/src/services/fs/serve_dir/tests.rs b/tower-http/src/services/fs/serve_dir/tests.rs index e0680be9..3023bdf8 100644 --- a/tower-http/src/services/fs/serve_dir/tests.rs +++ b/tower-http/src/services/fs/serve_dir/tests.rs @@ -893,6 +893,72 @@ async fn calls_fallback_on_null() { assert_eq!(res.headers()["from-fallback"], "1"); } +#[tokio::test] +async fn not_found_when_file_requested_with_trailing_slash() { + let svc = ServeDir::new("../test-files"); + + let req = Request::builder() + .uri("/index.html/") + .body(Body::empty()) + .unwrap(); + let res = svc.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::NOT_FOUND); + assert!(res.headers().get(header::CONTENT_TYPE).is_none()); + + let body = body_into_text(res.into_body()).await; + assert!(body.is_empty()); +} + +#[tokio::test] +async fn file_requested_with_trailing_slash_with_fallback() { + async fn fallback(req: Request) -> Result, Infallible> { + Ok(Response::new(Body::from(format!( + "from fallback {}", + req.uri().path() + )))) + } + + let svc = ServeDir::new("../test-files").fallback(tower::service_fn(fallback)); + + let req = Request::builder() + .uri("/index.html/") + .body(Body::empty()) + .unwrap(); + let res = svc.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + + let body = body_into_text(res.into_body()).await; + assert_eq!(body, "from fallback /index.html/"); +} + +#[tokio::test] +async fn directory_with_trailing_slash_appends_index_html() { + let svc = ServeDir::new("../test-files").append_index_html_on_directories(true); + let req = Request::builder().uri("/foo/").body(Body::empty()).unwrap(); + + let res = svc.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers()["content-type"], "text/html"); + let body = body_into_text(res.into_body()).await; + assert_eq!(body, "HTML!\n"); +} + +#[tokio::test] +async fn root_with_trailing_slash_serves_appends_index_html() { + let svc = ServeDir::new("../test-files").append_index_html_on_directories(true); + let req = Request::builder().uri("/").body(Body::empty()).unwrap(); + + let res = svc.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers()["content-type"], "text/html"); + let body = body_into_text(res.into_body()).await; + assert_eq!(body, "HTML!\n"); +} + #[cfg(windows)] fn verify_windows_device(name: &str, is_positive: bool) { use std::fs::OpenOptions; From 934b58efda7a327d13821c8f32ab4deff0bc6e56 Mon Sep 17 00:00:00 2001 From: Xiaoya Wei Date: Fri, 22 May 2026 14:30:12 +0800 Subject: [PATCH 2/2] refactor!: include grpc error message in tracing (#422) * refactor!: include grpc error message in tracing * Address review feedback on grpc error message extraction - Add #[repr(i32)] and #[non_exhaustive] to GrpcCode - Add #[non_exhaustive] to GrpcFailureClass - Fix behavioral regression: status codes >16 now correctly treated as non-success - Use decode_utf8_lossy() instead of discarding on invalid UTF-8 - Impl std::error::Error for GrpcFailureClass - Promote GrpcStatus methods to pub, add code_raw() and message() accessors - Export GrpcStatus from classify module - Add tests for percent-encoding, invalid UTF-8, unknown codes, and empty message * Make percent-encoding a required dependency and address review feedback - Remove cfg(feature = "trace") gate on percent-decoding: always use percent_decode().decode_utf8_lossy() for grpc-message extraction - Make percent-encoding a non-optional dependency to simplify the code - Add test for valid UTF-8 percent-encoded message - Add test that GrpcCode::Ok is classified as success via ClassifyResponse --------- Co-authored-by: Xiaoya Wei --- tower-http/Cargo.toml | 4 +- .../src/classify/grpc_errors_as_failures.rs | 327 ++++++++++++++---- tower-http/src/classify/mod.rs | 2 +- tower-http/src/trace/on_eos.rs | 6 +- tower-http/src/trace/on_response.rs | 6 +- 5 files changed, 271 insertions(+), 74 deletions(-) diff --git a/tower-http/Cargo.toml b/tower-http/Cargo.toml index 0cb95bf4..9a23c707 100644 --- a/tower-http/Cargo.toml +++ b/tower-http/Cargo.toml @@ -30,7 +30,7 @@ http-body-util = { version = "0.1.0", optional = true } http-range-header = { version = "0.4.0", optional = true } mime = { version = "0.3.17", optional = true, default-features = false } mime_guess = { version = "2", optional = true, default-features = false } -percent-encoding = { version = "2.1.0", optional = true } +percent-encoding = { version = "2.1.0" } url = { version = "2.5", optional = true } tokio = { version = "1.6", optional = true, default-features = false } tokio-util = { version = "0.7", optional = true, default-features = false, features = ["io"] } @@ -90,7 +90,7 @@ auth = ["base64", "validate-request"] catch-panic = ["tracing", "futures-util/std", "dep:http-body", "dep:http-body-util"] cors = [] follow-redirect = ["futures-util", "dep:http-body", "dep:url", "tower/util"] -fs = ["dep:tokio", "tokio?/fs", "tokio?/io-util", "futures-core", "futures-util", "dep:http-body", "dep:http-body-util", "tokio-util/io", "dep:http-range-header", "mime_guess", "mime", "percent-encoding", "httpdate", "set-status", "futures-util/alloc"] +fs = ["dep:tokio", "tokio?/fs", "tokio?/io-util", "futures-core", "futures-util", "dep:http-body", "dep:http-body-util", "tokio-util/io", "dep:http-range-header", "mime_guess", "mime", "httpdate", "set-status", "futures-util/alloc"] limit = ["dep:http-body", "dep:http-body-util"] map-request-body = [] map-response-body = [] diff --git a/tower-http/src/classify/grpc_errors_as_failures.rs b/tower-http/src/classify/grpc_errors_as_failures.rs index 417a884a..5e06d390 100644 --- a/tower-http/src/classify/grpc_errors_as_failures.rs +++ b/tower-http/src/classify/grpc_errors_as_failures.rs @@ -1,6 +1,7 @@ use super::{ClassifiedResponse, ClassifyEos, ClassifyResponse, SharedClassifier}; use bitflags::bitflags; use http::{HeaderMap, Response}; +use percent_encoding::percent_decode; use std::{fmt, num::NonZeroI32}; /// gRPC status codes. @@ -8,42 +9,44 @@ use std::{fmt, num::NonZeroI32}; /// These variants match the [gRPC status codes]. /// /// [gRPC status codes]: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(i32)] +#[non_exhaustive] pub enum GrpcCode { /// The operation completed successfully. - Ok, + Ok = 0, /// The operation was cancelled. - Cancelled, + Cancelled = 1, /// Unknown error. - Unknown, + Unknown = 2, /// Client specified an invalid argument. - InvalidArgument, + InvalidArgument = 3, /// Deadline expired before operation could complete. - DeadlineExceeded, + DeadlineExceeded = 4, /// Some requested entity was not found. - NotFound, + NotFound = 5, /// Some entity that we attempted to create already exists. - AlreadyExists, + AlreadyExists = 6, /// The caller does not have permission to execute the specified operation. - PermissionDenied, + PermissionDenied = 7, /// Some resource has been exhausted. - ResourceExhausted, + ResourceExhausted = 8, /// The system is not in a state required for the operation's execution. - FailedPrecondition, + FailedPrecondition = 9, /// The operation was aborted. - Aborted, + Aborted = 10, /// Operation was attempted past the valid range. - OutOfRange, + OutOfRange = 11, /// Operation is not implemented or not supported. - Unimplemented, + Unimplemented = 12, /// Internal error. - Internal, + Internal = 13, /// The service is currently unavailable. - Unavailable, + Unavailable = 14, /// Unrecoverable data loss or corruption. - DataLoss, + DataLoss = 15, /// The request does not have valid authentication credentials - Unauthenticated, + Unauthenticated = 16, } impl GrpcCode { @@ -68,6 +71,29 @@ impl GrpcCode { Self::Unauthenticated => GrpcCodeBitmask::UNAUTHENTICATED, } } + + fn from_i32(code: i32) -> Option { + match code { + 0 => Some(GrpcCode::Ok), + 1 => Some(GrpcCode::Cancelled), + 2 => Some(GrpcCode::Unknown), + 3 => Some(GrpcCode::InvalidArgument), + 4 => Some(GrpcCode::DeadlineExceeded), + 5 => Some(GrpcCode::NotFound), + 6 => Some(GrpcCode::AlreadyExists), + 7 => Some(GrpcCode::PermissionDenied), + 8 => Some(GrpcCode::ResourceExhausted), + 9 => Some(GrpcCode::FailedPrecondition), + 10 => Some(GrpcCode::Aborted), + 11 => Some(GrpcCode::OutOfRange), + 12 => Some(GrpcCode::Unimplemented), + 13 => Some(GrpcCode::Internal), + 14 => Some(GrpcCode::Unavailable), + 15 => Some(GrpcCode::DataLoss), + 16 => Some(GrpcCode::Unauthenticated), + _ => None, + } + } } /// Converts an `i32` gRPC status code into a [`GrpcCode`]. @@ -128,27 +154,26 @@ bitflags! { } } -impl GrpcCodeBitmask { - fn try_from_u32(code: u32) -> Option { +impl From for GrpcCodeBitmask { + fn from(code: GrpcCode) -> Self { match code { - 0 => Some(Self::OK), - 1 => Some(Self::CANCELLED), - 2 => Some(Self::UNKNOWN), - 3 => Some(Self::INVALID_ARGUMENT), - 4 => Some(Self::DEADLINE_EXCEEDED), - 5 => Some(Self::NOT_FOUND), - 6 => Some(Self::ALREADY_EXISTS), - 7 => Some(Self::PERMISSION_DENIED), - 8 => Some(Self::RESOURCE_EXHAUSTED), - 9 => Some(Self::FAILED_PRECONDITION), - 10 => Some(Self::ABORTED), - 11 => Some(Self::OUT_OF_RANGE), - 12 => Some(Self::UNIMPLEMENTED), - 13 => Some(Self::INTERNAL), - 14 => Some(Self::UNAVAILABLE), - 15 => Some(Self::DATA_LOSS), - 16 => Some(Self::UNAUTHENTICATED), - _ => None, + GrpcCode::Ok => GrpcCodeBitmask::OK, + GrpcCode::Cancelled => GrpcCodeBitmask::CANCELLED, + GrpcCode::Unknown => GrpcCodeBitmask::UNKNOWN, + GrpcCode::InvalidArgument => GrpcCodeBitmask::INVALID_ARGUMENT, + GrpcCode::DeadlineExceeded => GrpcCodeBitmask::DEADLINE_EXCEEDED, + GrpcCode::NotFound => GrpcCodeBitmask::NOT_FOUND, + GrpcCode::AlreadyExists => GrpcCodeBitmask::ALREADY_EXISTS, + GrpcCode::PermissionDenied => GrpcCodeBitmask::PERMISSION_DENIED, + GrpcCode::ResourceExhausted => GrpcCodeBitmask::RESOURCE_EXHAUSTED, + GrpcCode::FailedPrecondition => GrpcCodeBitmask::FAILED_PRECONDITION, + GrpcCode::Aborted => GrpcCodeBitmask::ABORTED, + GrpcCode::OutOfRange => GrpcCodeBitmask::OUT_OF_RANGE, + GrpcCode::Unimplemented => GrpcCodeBitmask::UNIMPLEMENTED, + GrpcCode::Internal => GrpcCodeBitmask::INTERNAL, + GrpcCode::Unavailable => GrpcCodeBitmask::UNAVAILABLE, + GrpcCode::DataLoss => GrpcCodeBitmask::DATA_LOSS, + GrpcCode::Unauthenticated => GrpcCodeBitmask::UNAUTHENTICATED, } } } @@ -225,11 +250,11 @@ impl ClassifyResponse for GrpcErrorsAsFailures { res: &Response, ) -> ClassifiedResponse { match classify_grpc_metadata(res.headers(), self.success_codes) { - ParsedGrpcStatus::Success - | ParsedGrpcStatus::HeaderNotString - | ParsedGrpcStatus::HeaderNotInt => ClassifiedResponse::Ready(Ok(())), + ParsedGrpcStatus::Success | ParsedGrpcStatus::HeaderNotGrpcCode => { + ClassifiedResponse::Ready(Ok(())) + } ParsedGrpcStatus::NonSuccess(status) => { - ClassifiedResponse::Ready(Err(GrpcFailureClass::Code(status))) + ClassifiedResponse::Ready(Err(GrpcFailureClass::Status(status))) } ParsedGrpcStatus::GrpcStatusHeaderMissing => { ClassifiedResponse::RequiresEos(GrpcEosErrorsAsFailures { @@ -261,9 +286,8 @@ impl ClassifyEos for GrpcEosErrorsAsFailures { match classify_grpc_metadata(trailers, self.success_codes) { ParsedGrpcStatus::Success | ParsedGrpcStatus::GrpcStatusHeaderMissing - | ParsedGrpcStatus::HeaderNotString - | ParsedGrpcStatus::HeaderNotInt => Ok(()), - ParsedGrpcStatus::NonSuccess(status) => Err(GrpcFailureClass::Code(status)), + | ParsedGrpcStatus::HeaderNotGrpcCode => Ok(()), + ParsedGrpcStatus::NonSuccess(status) => Err(GrpcFailureClass::Status(status)), } } else { Ok(()) @@ -280,9 +304,10 @@ impl ClassifyEos for GrpcEosErrorsAsFailures { /// The failure class for [`GrpcErrorsAsFailures`]. #[derive(Debug)] +#[non_exhaustive] pub enum GrpcFailureClass { /// A gRPC response was classified as a failure with the corresponding status. - Code(std::num::NonZeroI32), + Status(GrpcStatus), /// A gRPC response was classified as an error with the corresponding error description. Error(String), } @@ -290,12 +315,16 @@ pub enum GrpcFailureClass { impl fmt::Display for GrpcFailureClass { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::Code(code) => write!(f, "Code: {}", code), + Self::Status(status) => { + write!(f, "Status: {}", status) + } Self::Error(error) => write!(f, "Error: {}", error), } } } +impl std::error::Error for GrpcFailureClass {} + pub(crate) fn classify_grpc_metadata( headers: &HeaderMap, success_codes: GrpcCodeBitmask, @@ -310,28 +339,77 @@ pub(crate) fn classify_grpc_metadata( }; } - let status = or_else!(headers.get("grpc-status"), GrpcStatusHeaderMissing); - let status = or_else!(status.to_str().ok(), HeaderNotString); - let status = or_else!(status.parse::().ok(), HeaderNotInt); + let code_header = or_else!(headers.get("grpc-status"), GrpcStatusHeaderMissing); + let code_value: i32 = or_else!( + code_header.to_str().ok().and_then(|s| s.parse().ok()), + HeaderNotGrpcCode + ); + let grpc_code = GrpcCode::from_i32(code_value); - if GrpcCodeBitmask::try_from_u32(status as _) - .filter(|code| success_codes.contains(*code)) - .is_some() - { - ParsedGrpcStatus::Success - } else { - ParsedGrpcStatus::NonSuccess(NonZeroI32::new(status).unwrap()) + if let Some(code) = grpc_code { + if success_codes.contains(GrpcCodeBitmask::from(code)) { + return ParsedGrpcStatus::Success; + } + } + + let message = headers.get("grpc-message").map(|header| { + percent_decode(header.as_bytes()) + .decode_utf8_lossy() + .into_owned() + }); + + ParsedGrpcStatus::NonSuccess(GrpcStatus { + code: grpc_code, + code_raw: code_value, + message, + }) +} + +/// A gRPC status extracted from response headers/trailers. +#[derive(Debug, PartialEq, Eq)] +pub struct GrpcStatus { + code: Option, + code_raw: i32, + message: Option, +} + +impl GrpcStatus { + /// Returns the status code as a [`GrpcCode`], or `None` if the code is not recognized. + pub fn code(&self) -> Option { + self.code + } + + /// Returns the raw integer status code. + pub fn code_raw(&self) -> i32 { + self.code_raw + } + + /// Returns the percent-decoded gRPC error message, if present. + pub fn message(&self) -> Option<&str> { + self.message.as_deref() + } +} + +impl fmt::Display for GrpcStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.code { + Some(code) => write!(f, "{:?}", code)?, + None => write!(f, "Code({})", self.code_raw)?, + } + if let Some(message) = self.message.as_ref() { + write!(f, ": {}", message)?; + } + Ok(()) } } #[derive(Debug, PartialEq, Eq)] pub(crate) enum ParsedGrpcStatus { Success, - NonSuccess(NonZeroI32), + NonSuccess(GrpcStatus), GrpcStatusHeaderMissing, - // these two are treated as `Success` but kept separate for clarity - HeaderNotString, - HeaderNotInt, + // this is treated as `Success` but kept separate for clarity + HeaderNotGrpcCode, } #[cfg(test)] @@ -344,11 +422,29 @@ mod tests { status: $status:expr, success_flags: $success_flags:expr, expected: $expected:expr, + ) => { + classify_grpc_metadata_test!( + name: $name, + status: $status, + message: "", + success_flags: $success_flags, + expected: $expected, + ); + }; + ( + name: $name:ident, + status: $status:expr, + message: $message:expr, + success_flags: $success_flags:expr, + expected: $expected:expr, ) => { #[test] fn $name() { let mut headers = HeaderMap::new(); headers.insert("grpc-status", $status.parse().unwrap()); + if !$message.is_empty() { + headers.insert("grpc-message", $message.parse().unwrap()); + } let status = classify_grpc_metadata(&headers, $success_flags); assert_eq!(status, $expected); } @@ -366,7 +462,11 @@ mod tests { name: basic_error, status: "1", success_flags: GrpcCodeBitmask::OK, - expected: ParsedGrpcStatus::NonSuccess(NonZeroI32::new(1).unwrap()), + expected: ParsedGrpcStatus::NonSuccess(GrpcStatus{ + code: Some(GrpcCode::Cancelled), + code_raw: 1, + message: None, + }), } classify_grpc_metadata_test! { @@ -386,8 +486,109 @@ mod tests { classify_grpc_metadata_test! { name: two_success_codes_none_matches, status: "16", + message: "mock message", success_flags: GrpcCodeBitmask::OK | GrpcCodeBitmask::INVALID_ARGUMENT, - expected: ParsedGrpcStatus::NonSuccess(NonZeroI32::new(16).unwrap()), + expected: ParsedGrpcStatus::NonSuccess(GrpcStatus{ + code: Some(GrpcCode::Unauthenticated), + code_raw: 16, + message: Some("mock message".to_string()), + }), + } + + classify_grpc_metadata_test! { + name: percent_encoded_message, + status: "2", + message: "hello%20world", + success_flags: GrpcCodeBitmask::OK, + expected: ParsedGrpcStatus::NonSuccess(GrpcStatus{ + code: Some(GrpcCode::Unknown), + code_raw: 2, + message: Some("hello world".to_string()), + }), + } + + classify_grpc_metadata_test! { + name: invalid_percent_encoding, + status: "13", + message: "bad%2Gencode", + success_flags: GrpcCodeBitmask::OK, + expected: ParsedGrpcStatus::NonSuccess(GrpcStatus{ + code: Some(GrpcCode::Internal), + code_raw: 13, + message: Some("bad%2Gencode".to_string()), + }), + } + + classify_grpc_metadata_test! { + name: empty_grpc_message, + status: "5", + message: "", + success_flags: GrpcCodeBitmask::OK, + expected: ParsedGrpcStatus::NonSuccess(GrpcStatus{ + code: Some(GrpcCode::NotFound), + code_raw: 5, + message: None, + }), + } + + classify_grpc_metadata_test! { + name: unknown_status_code_above_16, + status: "99", + message: "custom error", + success_flags: GrpcCodeBitmask::OK, + expected: ParsedGrpcStatus::NonSuccess(GrpcStatus{ + code: None, + code_raw: 99, + message: Some("custom error".to_string()), + }), + } + + #[test] + fn invalid_utf8_after_percent_decode() { + let mut headers = HeaderMap::new(); + headers.insert("grpc-status", "2".parse().unwrap()); + // %80 is an invalid UTF-8 start byte; lossy decode replaces it with U+FFFD + headers.insert("grpc-message", "bad%80byte".parse().unwrap()); + let status = classify_grpc_metadata(&headers, GrpcCodeBitmask::OK); + assert_eq!( + status, + ParsedGrpcStatus::NonSuccess(GrpcStatus { + code: Some(GrpcCode::Unknown), + code_raw: 2, + message: Some("bad\u{FFFD}byte".to_string()), + }) + ); + } + + #[test] + fn valid_utf8_percent_encoded() { + let mut headers = HeaderMap::new(); + headers.insert("grpc-status", "3".parse().unwrap()); + // %C3%A9 is the percent-encoded form of 'é' (U+00E9) in UTF-8 + headers.insert("grpc-message", "caf%C3%A9".parse().unwrap()); + let status = classify_grpc_metadata(&headers, GrpcCodeBitmask::OK); + assert_eq!( + status, + ParsedGrpcStatus::NonSuccess(GrpcStatus { + code: Some(GrpcCode::InvalidArgument), + code_raw: 3, + message: Some("café".to_string()), + }) + ); + } + + #[test] + fn grpc_ok_classified_as_success() { + use http::Response; + + let res = Response::builder() + .header("grpc-status", "0") + .body(()) + .unwrap(); + + let classifier = GrpcErrorsAsFailures::new(); + let result = classifier.classify_response(&res); + assert!(matches!(result, ClassifiedResponse::Ready(Ok(())))); } #[test] diff --git a/tower-http/src/classify/mod.rs b/tower-http/src/classify/mod.rs index a3147843..49a203f2 100644 --- a/tower-http/src/classify/mod.rs +++ b/tower-http/src/classify/mod.rs @@ -9,7 +9,7 @@ mod status_in_range_is_error; pub use self::{ grpc_errors_as_failures::{ - GrpcCode, GrpcEosErrorsAsFailures, GrpcErrorsAsFailures, GrpcFailureClass, + GrpcCode, GrpcEosErrorsAsFailures, GrpcErrorsAsFailures, GrpcFailureClass, GrpcStatus, }, map_failure_class::MapFailureClass, status_in_range_is_error::{StatusInRangeAsFailures, StatusInRangeFailureClass}, diff --git a/tower-http/src/trace/on_eos.rs b/tower-http/src/trace/on_eos.rs index ab90fc9c..95788d7e 100644 --- a/tower-http/src/trace/on_eos.rs +++ b/tower-http/src/trace/on_eos.rs @@ -94,10 +94,8 @@ impl OnEos for DefaultOnEos { trailers, crate::classify::GrpcCode::Ok.into_bitmask(), ) { - ParsedGrpcStatus::Success - | ParsedGrpcStatus::HeaderNotString - | ParsedGrpcStatus::HeaderNotInt => Some(0), - ParsedGrpcStatus::NonSuccess(status) => Some(status.get()), + ParsedGrpcStatus::Success | ParsedGrpcStatus::HeaderNotGrpcCode => Some(0), + ParsedGrpcStatus::NonSuccess(status) => Some(status.code_raw()), ParsedGrpcStatus::GrpcStatusHeaderMissing => None, } }); diff --git a/tower-http/src/trace/on_response.rs b/tower-http/src/trace/on_response.rs index c6ece840..edcf498b 100644 --- a/tower-http/src/trace/on_response.rs +++ b/tower-http/src/trace/on_response.rs @@ -147,10 +147,8 @@ fn status(res: &Response) -> Option { res.headers(), crate::classify::GrpcCode::Ok.into_bitmask(), ) { - ParsedGrpcStatus::Success - | ParsedGrpcStatus::HeaderNotString - | ParsedGrpcStatus::HeaderNotInt => Some(0), - ParsedGrpcStatus::NonSuccess(status) => Some(status.get()), + ParsedGrpcStatus::Success | ParsedGrpcStatus::HeaderNotGrpcCode => Some(0), + ParsedGrpcStatus::NonSuccess(status) => Some(status.code_raw()), // if `grpc-status` is missing then its a streaming response and there is no status // _yet_, so its neither success nor error ParsedGrpcStatus::GrpcStatusHeaderMissing => None,