Skip to content
Open
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
4 changes: 4 additions & 0 deletions tonic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- *(codec)* respect server's enabled encodings when selecting response compression, fixing a case where a server configured with `send_compressed(Zstd)` would still gzip responses when the client listed `gzip` before `zstd` in `grpc-accept-encoding`

## [0.14.6](https://github.com/hyperium/tonic/compare/tonic-v0.14.5...tonic-v0.14.6) - 2026-05-06

### Added
Expand Down
93 changes: 90 additions & 3 deletions tonic/src/codec/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,17 @@ impl CompressionEncoding {

split_by_comma(header_value_str).find_map(|value| match value {
#[cfg(feature = "gzip")]
"gzip" => Some(CompressionEncoding::Gzip),
"gzip" if enabled_encodings.is_enabled(CompressionEncoding::Gzip) => {
Some(CompressionEncoding::Gzip)
}
#[cfg(feature = "deflate")]
"deflate" => Some(CompressionEncoding::Deflate),
"deflate" if enabled_encodings.is_enabled(CompressionEncoding::Deflate) => {
Some(CompressionEncoding::Deflate)
}
#[cfg(feature = "zstd")]
"zstd" => Some(CompressionEncoding::Zstd),
"zstd" if enabled_encodings.is_enabled(CompressionEncoding::Zstd) => {
Some(CompressionEncoding::Zstd)
}
_ => None,
})
}
Expand Down Expand Up @@ -357,6 +363,87 @@ mod tests {
assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD);
}

#[test]
#[cfg(all(feature = "gzip", feature = "zstd"))]
fn from_accept_encoding_header_respects_server_enabled_encodings() {
// Regression test for the case where the client advertises multiple
// encodings in its `grpc-accept-encoding` header but the server only
// enabled a subset of them via `.send_compressed(...)`.
//
// Previously, `from_accept_encoding_header` would pick the first
// encoding in the client's list whose `cfg(feature = ...)` was
// compiled in, without checking whether the server actually enabled
// that encoding. That meant a server configured for Zstd-only would
// gzip its responses whenever a client listed `gzip` before `zstd`
// in `grpc-accept-encoding` — even though the server never asked for
// gzip and (in `into_accept_encoding_header_value`) would never have
// advertised it.
//
// The selected encoding must come from the intersection of the
// server's enabled set and the client's accept list, preserving the
// client's preference order.
let mut enabled = EnabledCompressionEncodings::default();
enabled.enable(CompressionEncoding::Zstd);
assert!(enabled.is_enabled(CompressionEncoding::Zstd));
assert!(!enabled.is_enabled(CompressionEncoding::Gzip));

let mut headers = http::HeaderMap::new();
headers.insert(
ACCEPT_ENCODING_HEADER,
HeaderValue::from_static("gzip,zstd,identity"),
);

assert_eq!(
CompressionEncoding::from_accept_encoding_header(&headers, enabled),
Some(CompressionEncoding::Zstd),
"server has only Zstd enabled; must not pick Gzip just because \
the client listed it first",
);
}

#[test]
#[cfg(all(feature = "gzip", feature = "zstd"))]
fn from_accept_encoding_header_returns_none_when_no_overlap() {
// If the client's `grpc-accept-encoding` and the server's enabled
// encodings have no overlap, no compression should be selected — the
// server should fall back to sending the response uncompressed.
let mut enabled = EnabledCompressionEncodings::default();
enabled.enable(CompressionEncoding::Zstd);

let mut headers = http::HeaderMap::new();
headers.insert(
ACCEPT_ENCODING_HEADER,
HeaderValue::from_static("gzip,identity"),
);

assert_eq!(
CompressionEncoding::from_accept_encoding_header(&headers, enabled),
None,
);
}

#[test]
#[cfg(all(feature = "gzip", feature = "zstd"))]
fn from_accept_encoding_header_uses_client_preference_order() {
// When multiple enabled encodings appear in the client's accept list,
// the client's ordering wins. Both encodings are enabled on the
// server, but the client prefers gzip.
let mut enabled = EnabledCompressionEncodings::default();
enabled.enable(CompressionEncoding::Zstd);
enabled.enable(CompressionEncoding::Gzip);

let mut headers = http::HeaderMap::new();
headers.insert(
ACCEPT_ENCODING_HEADER,
HeaderValue::from_static("gzip,zstd,identity"),
);

assert_eq!(
CompressionEncoding::from_accept_encoding_header(&headers, enabled),
Some(CompressionEncoding::Gzip),
);
}

#[test]
#[cfg(all(feature = "gzip", feature = "deflate", feature = "zstd"))]
fn convert_compression_encodings_into_header_value() {
Expand Down
Loading