Skip to content
Merged
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
11 changes: 11 additions & 0 deletions noextras/test/test_compression_default.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import pytest
from connectrpc._compression import get_accept_encoding
from example.eliza_connect import (
ElizaService,
ElizaServiceASGIApplication,
Expand Down Expand Up @@ -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}'"
9 changes: 7 additions & 2 deletions src/connectrpc/_client_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions src/connectrpc/_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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())
Expand Down
Loading