diff --git a/noextras/test/test_compression_default.py b/noextras/test/test_compression_default.py index fbcc09c..d94d4ac 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 from example.eliza_connect import ( ElizaService, ElizaServiceASGIApplication, @@ -94,3 +95,13 @@ 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'. + """ + 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 18cb166..6ddd618 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, + get_available_compressions, + get_compression, +) from ._protocol import ConnectWireError from ._protocol_connect import ( CONNECT_PROTOCOL_VERSION, @@ -88,7 +93,7 @@ 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] = 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 090f043..7a5fe6d 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,6 +105,17 @@ def get_available_compressions() -> KeysView: return _compressions.keys() +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). + """ + return ", ".join( + name for name in DEFAULT_ACCEPT_ENCODING_COMPRESSIONS if name in _compressions + ) + + def negotiate_compression(accept_encoding: str) -> Compression: for accept in accept_encoding.split(","): compression = _compressions.get(accept.strip())