From f2747eb07f9744f491c3773d6c566858ba9cb3c5 Mon Sep 17 00:00:00 2001 From: Achille Roussel Date: Tue, 23 Dec 2025 23:45:16 -0800 Subject: [PATCH 1/3] set default accept-encoding to compression algorithms supported by the client Signed-off-by: Achille Roussel --- noextras/test/test_compression_default.py | 14 ++++++++++++++ src/connectrpc/_client_shared.py | 11 +++++++++-- src/connectrpc/_compression.py | 10 ++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/noextras/test/test_compression_default.py b/noextras/test/test_compression_default.py index fbcc09c..6f6d353 100644 --- a/noextras/test/test_compression_default.py +++ b/noextras/test/test_compression_default.py @@ -1,6 +1,7 @@ from __future__ import annotations import pytest +from connectrpc._compression import get_accept_encoding_compressions from example.eliza_connect import ( ElizaService, ElizaServiceASGIApplication, @@ -94,3 +95,16 @@ async def say(self, request, ctx): str(exc_info.value) == f"Unsupported compression method: {compression}. Available methods: gzip, identity" ) + + +def test_accept_encoding_only_includes_available_compressions(): + """Verify Accept-Encoding only advertises compressions that are actually available. + + When brotli and zstandard are not installed (as in the noextras environment), + the Accept-Encoding header should not include 'br' or 'zstd'. + """ + available = get_accept_encoding_compressions() + assert "br" not in available, "brotli should not be advertised when not installed" + assert "zstd" not in available, "zstd should not be advertised when not installed" + assert "gzip" in available, "gzip should always be available" + assert "identity" not in available, "identity should not be in Accept-Encoding" diff --git a/src/connectrpc/_client_shared.py b/src/connectrpc/_client_shared.py index 18cb166..e8d0049 100644 --- a/src/connectrpc/_client_shared.py +++ b/src/connectrpc/_client_shared.py @@ -6,7 +6,12 @@ from . import _compression from ._codec import CODEC_NAME_JSON, CODEC_NAME_JSON_CHARSET_UTF8, Codec -from ._compression import Compression, get_available_compressions, get_compression +from ._compression import ( + Compression, + get_accept_encoding_compressions, + get_available_compressions, + get_compression, +) from ._protocol import ConnectWireError from ._protocol_connect import ( CONNECT_PROTOCOL_VERSION, @@ -88,7 +93,9 @@ def create_request_context( if accept_compression is not None: headers[accept_compression_header] = ", ".join(accept_compression) else: - headers[accept_compression_header] = "gzip, br, zstd" + headers[accept_compression_header] = ", ".join( + get_accept_encoding_compressions() + ) if send_compression is not None: headers[compression_header] = send_compression.name() else: diff --git a/src/connectrpc/_compression.py b/src/connectrpc/_compression.py index 090f043..bb085da 100644 --- a/src/connectrpc/_compression.py +++ b/src/connectrpc/_compression.py @@ -101,6 +101,16 @@ def get_available_compressions() -> KeysView: return _compressions.keys() +def get_accept_encoding_compressions() -> list[str]: + """Returns compression names suitable for Accept-Encoding header, in preference order. + + This excludes 'identity' since it's an implicit fallback, and returns + only compressions that are actually available (i.e., their dependencies are installed). + """ + preferred_order = ["gzip", "br", "zstd"] + return [name for name in preferred_order if name in _compressions] + + def negotiate_compression(accept_encoding: str) -> Compression: for accept in accept_encoding.split(","): compression = _compressions.get(accept.strip()) From c9cabe1b426b71ba9d330ec45a821bae56fe4495 Mon Sep 17 00:00:00 2001 From: Achille Roussel Date: Wed, 24 Dec 2025 11:22:42 -0800 Subject: [PATCH 2/3] PR feedback Signed-off-by: Achille Roussel --- noextras/test/test_compression_default.py | 9 +++------ src/connectrpc/_client_shared.py | 6 ++---- src/connectrpc/_compression.py | 13 +++++++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/noextras/test/test_compression_default.py b/noextras/test/test_compression_default.py index 6f6d353..d94d4ac 100644 --- a/noextras/test/test_compression_default.py +++ b/noextras/test/test_compression_default.py @@ -1,7 +1,7 @@ from __future__ import annotations import pytest -from connectrpc._compression import get_accept_encoding_compressions +from connectrpc._compression import get_accept_encoding from example.eliza_connect import ( ElizaService, ElizaServiceASGIApplication, @@ -103,8 +103,5 @@ def test_accept_encoding_only_includes_available_compressions(): When brotli and zstandard are not installed (as in the noextras environment), the Accept-Encoding header should not include 'br' or 'zstd'. """ - available = get_accept_encoding_compressions() - assert "br" not in available, "brotli should not be advertised when not installed" - assert "zstd" not in available, "zstd should not be advertised when not installed" - assert "gzip" in available, "gzip should always be available" - assert "identity" not in available, "identity should not be in Accept-Encoding" + accept_encoding = get_accept_encoding() + assert accept_encoding == "gzip", f"Expected 'gzip' only, got '{accept_encoding}'" diff --git a/src/connectrpc/_client_shared.py b/src/connectrpc/_client_shared.py index e8d0049..6ddd618 100644 --- a/src/connectrpc/_client_shared.py +++ b/src/connectrpc/_client_shared.py @@ -8,7 +8,7 @@ from ._codec import CODEC_NAME_JSON, CODEC_NAME_JSON_CHARSET_UTF8, Codec from ._compression import ( Compression, - get_accept_encoding_compressions, + get_accept_encoding, get_available_compressions, get_compression, ) @@ -93,9 +93,7 @@ def create_request_context( if accept_compression is not None: headers[accept_compression_header] = ", ".join(accept_compression) else: - headers[accept_compression_header] = ", ".join( - get_accept_encoding_compressions() - ) + headers[accept_compression_header] = get_accept_encoding() if send_compression is not None: headers[compression_header] = send_compression.name() else: diff --git a/src/connectrpc/_compression.py b/src/connectrpc/_compression.py index bb085da..c451e77 100644 --- a/src/connectrpc/_compression.py +++ b/src/connectrpc/_compression.py @@ -91,6 +91,10 @@ def decompress(self, data: bytes | bytearray) -> bytes: _identity = IdentityCompression() _compressions["identity"] = _identity +# Preferred compression names for Accept-Encoding header, in order of preference. +# Excludes 'identity' since it's an implicit fallback. +DEFAULT_ACCEPT_ENCODING_COMPRESSIONS = ["gzip", "br", "zstd"] + def get_compression(name: str) -> Compression | None: return _compressions.get(name.lower()) @@ -101,14 +105,15 @@ def get_available_compressions() -> KeysView: return _compressions.keys() -def get_accept_encoding_compressions() -> list[str]: - """Returns compression names suitable for Accept-Encoding header, in preference order. +def get_accept_encoding() -> str: + """Returns Accept-Encoding header value with available compressions in preference order. This excludes 'identity' since it's an implicit fallback, and returns only compressions that are actually available (i.e., their dependencies are installed). """ - preferred_order = ["gzip", "br", "zstd"] - return [name for name in preferred_order if name in _compressions] + return ", ".join( + name for name in DEFAULT_ACCEPT_ENCODING_COMPRESSIONS if name in _compressions + ) def negotiate_compression(accept_encoding: str) -> Compression: From daa20f3258cf21e47937d689dab270345695f9d1 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 25 Dec 2025 13:48:19 +0900 Subject: [PATCH 3/3] Small immutability cleanup Signed-off-by: Anuraag Agrawal --- src/connectrpc/_compression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connectrpc/_compression.py b/src/connectrpc/_compression.py index c451e77..7a5fe6d 100644 --- a/src/connectrpc/_compression.py +++ b/src/connectrpc/_compression.py @@ -93,7 +93,7 @@ def decompress(self, data: bytes | bytearray) -> bytes: # Preferred compression names for Accept-Encoding header, in order of preference. # Excludes 'identity' since it's an implicit fallback. -DEFAULT_ACCEPT_ENCODING_COMPRESSIONS = ["gzip", "br", "zstd"] +DEFAULT_ACCEPT_ENCODING_COMPRESSIONS = ("gzip", "br", "zstd") def get_compression(name: str) -> Compression | None: