Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 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_compressions
from example.eliza_connect import (
ElizaService,
ElizaServiceASGIApplication,
Expand Down Expand Up @@ -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"
11 changes: 9 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_compressions,
get_available_compressions,
get_compression,
)
from ._protocol import ConnectWireError
from ._protocol_connect import (
CONNECT_PROTOCOL_VERSION,
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions src/connectrpc/_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this out of the function, maybe DEFAULT_ACCEPT_ENCODING_COMPRESSIONS

return [name for name in preferred_order if name in _compressions]
Copy link
Collaborator

@anuraaga anuraaga Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about returning the joined string without the intermediate list? A microoptimization, but the list is only benefiting the unit test currently IIUC



def negotiate_compression(accept_encoding: str) -> Compression:
for accept in accept_encoding.split(","):
compression = _compressions.get(accept.strip())
Expand Down