Skip to content

Commit 396063a

Browse files
author
Vineeth Rao Kanaparthi
committed
fix(codec): respect server's enabled encodings when selecting response compression
`CompressionEncoding::from_accept_encoding_header` picks an encoding from the client's `grpc-accept-encoding` header gated only on `cfg(feature = ...)`, not on whether the server actually enabled that encoding via `.send_compressed(...)`. A server configured for Zstd would gzip every response whenever a client listed `gzip` before `zstd` in `grpc-accept-encoding` -- even though the server never asked for gzip and would not have advertised it in `into_accept_encoding_header_value`. The sibling `from_encoding_header` already guards each match arm on `enabled_encodings.is_enabled(...)`; this fix brings the accept-side in line. Observed in production: tonic 0.14.6 server set to `send_compressed(Zstd)` only, clients sending `gzip,zstd,identity`, ~38% of server CPU spent in `miniz_oxide::deflate`. Adds three unit tests covering: * server-only-Zstd, client lists gzip first -> picks Zstd * no overlap between server and client lists -> picks None * both enabled, client prefers gzip -> picks Gzip
1 parent af47335 commit 396063a

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

tonic/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- *(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`
13+
1014
## [0.14.6](https://github.com/hyperium/tonic/compare/tonic-v0.14.5...tonic-v0.14.6) - 2026-05-06
1115

1216
### Added

tonic/src/codec/compression.rs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,17 @@ impl CompressionEncoding {
118118

119119
split_by_comma(header_value_str).find_map(|value| match value {
120120
#[cfg(feature = "gzip")]
121-
"gzip" => Some(CompressionEncoding::Gzip),
121+
"gzip" if enabled_encodings.is_enabled(CompressionEncoding::Gzip) => {
122+
Some(CompressionEncoding::Gzip)
123+
}
122124
#[cfg(feature = "deflate")]
123-
"deflate" => Some(CompressionEncoding::Deflate),
125+
"deflate" if enabled_encodings.is_enabled(CompressionEncoding::Deflate) => {
126+
Some(CompressionEncoding::Deflate)
127+
}
124128
#[cfg(feature = "zstd")]
125-
"zstd" => Some(CompressionEncoding::Zstd),
129+
"zstd" if enabled_encodings.is_enabled(CompressionEncoding::Zstd) => {
130+
Some(CompressionEncoding::Zstd)
131+
}
126132
_ => None,
127133
})
128134
}
@@ -357,6 +363,87 @@ mod tests {
357363
assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD);
358364
}
359365

366+
#[test]
367+
#[cfg(all(feature = "gzip", feature = "zstd"))]
368+
fn from_accept_encoding_header_respects_server_enabled_encodings() {
369+
// Regression test for the case where the client advertises multiple
370+
// encodings in its `grpc-accept-encoding` header but the server only
371+
// enabled a subset of them via `.send_compressed(...)`.
372+
//
373+
// Previously, `from_accept_encoding_header` would pick the first
374+
// encoding in the client's list whose `cfg(feature = ...)` was
375+
// compiled in, without checking whether the server actually enabled
376+
// that encoding. That meant a server configured for Zstd-only would
377+
// gzip its responses whenever a client listed `gzip` before `zstd`
378+
// in `grpc-accept-encoding` — even though the server never asked for
379+
// gzip and (in `into_accept_encoding_header_value`) would never have
380+
// advertised it.
381+
//
382+
// The selected encoding must come from the intersection of the
383+
// server's enabled set and the client's accept list, preserving the
384+
// client's preference order.
385+
let mut enabled = EnabledCompressionEncodings::default();
386+
enabled.enable(CompressionEncoding::Zstd);
387+
assert!(enabled.is_enabled(CompressionEncoding::Zstd));
388+
assert!(!enabled.is_enabled(CompressionEncoding::Gzip));
389+
390+
let mut headers = http::HeaderMap::new();
391+
headers.insert(
392+
ACCEPT_ENCODING_HEADER,
393+
HeaderValue::from_static("gzip,zstd,identity"),
394+
);
395+
396+
assert_eq!(
397+
CompressionEncoding::from_accept_encoding_header(&headers, enabled),
398+
Some(CompressionEncoding::Zstd),
399+
"server has only Zstd enabled; must not pick Gzip just because \
400+
the client listed it first",
401+
);
402+
}
403+
404+
#[test]
405+
#[cfg(all(feature = "gzip", feature = "zstd"))]
406+
fn from_accept_encoding_header_returns_none_when_no_overlap() {
407+
// If the client's `grpc-accept-encoding` and the server's enabled
408+
// encodings have no overlap, no compression should be selected — the
409+
// server should fall back to sending the response uncompressed.
410+
let mut enabled = EnabledCompressionEncodings::default();
411+
enabled.enable(CompressionEncoding::Zstd);
412+
413+
let mut headers = http::HeaderMap::new();
414+
headers.insert(
415+
ACCEPT_ENCODING_HEADER,
416+
HeaderValue::from_static("gzip,identity"),
417+
);
418+
419+
assert_eq!(
420+
CompressionEncoding::from_accept_encoding_header(&headers, enabled),
421+
None,
422+
);
423+
}
424+
425+
#[test]
426+
#[cfg(all(feature = "gzip", feature = "zstd"))]
427+
fn from_accept_encoding_header_uses_client_preference_order() {
428+
// When multiple enabled encodings appear in the client's accept list,
429+
// the client's ordering wins. Both encodings are enabled on the
430+
// server, but the client prefers gzip.
431+
let mut enabled = EnabledCompressionEncodings::default();
432+
enabled.enable(CompressionEncoding::Zstd);
433+
enabled.enable(CompressionEncoding::Gzip);
434+
435+
let mut headers = http::HeaderMap::new();
436+
headers.insert(
437+
ACCEPT_ENCODING_HEADER,
438+
HeaderValue::from_static("gzip,zstd,identity"),
439+
);
440+
441+
assert_eq!(
442+
CompressionEncoding::from_accept_encoding_header(&headers, enabled),
443+
Some(CompressionEncoding::Gzip),
444+
);
445+
}
446+
360447
#[test]
361448
#[cfg(all(feature = "gzip", feature = "deflate", feature = "zstd"))]
362449
fn convert_compression_encodings_into_header_value() {

0 commit comments

Comments
 (0)