Skip to content

Commit 1a0a555

Browse files
committed
feat: warn when brotli extra missing
1 parent def4778 commit 1a0a555

3 files changed

Lines changed: 39 additions & 6 deletions

File tree

httpx/_decoders.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@
2626
brotli = None
2727

2828

29+
BROTLI_INSTALLED = brotli is not None
30+
2931
# Zstandard support is optional
3032
try:
3133
import zstandard
3234
except ImportError: # pragma: no cover
3335
zstandard = None # type: ignore
3436

37+
ZSTANDARD_INSTALLED = zstandard is not None
3538

3639
class ContentDecoder:
3740
def decode(self, data: bytes) -> bytes:
@@ -387,7 +390,7 @@ def flush(self) -> list[str]:
387390
}
388391

389392

390-
if brotli is None:
393+
if not BROTLI_INSTALLED:
391394
SUPPORTED_DECODERS.pop("br") # pragma: no cover
392-
if zstandard is None:
395+
if not ZSTANDARD_INSTALLED:
393396
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover

httpx/_models.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import re
88
import typing
99
import urllib.request
10+
import warnings
1011
from collections.abc import Mapping
1112
from http.cookiejar import Cookie, CookieJar
1213

1314
from ._content import ByteStream, UnattachedStream, encode_request, encode_response
1415
from ._decoders import (
16+
BROTLI_INSTALLED,
1517
SUPPORTED_DECODERS,
1618
ByteChunker,
1719
ContentDecoder,
@@ -704,13 +706,25 @@ def _get_content_decoder(self) -> ContentDecoder:
704706
if not hasattr(self, "_decoder"):
705707
decoders: list[ContentDecoder] = []
706708
values = self.headers.get_list("content-encoding", split_commas=True)
709+
warned_missing_brotli = False
707710
for value in values:
708711
value = value.strip().lower()
709-
try:
710-
decoder_cls = SUPPORTED_DECODERS[value]
711-
decoders.append(decoder_cls())
712-
except KeyError:
712+
decoder_cls = SUPPORTED_DECODERS.get(value)
713+
if decoder_cls is None:
714+
if (
715+
value == "br"
716+
and not BROTLI_INSTALLED
717+
and not warned_missing_brotli
718+
):
719+
warned_missing_brotli = True
720+
warnings.warn(
721+
"Received 'Content-Encoding: br' but Brotli support is disabled. "
722+
"Install httpx[brotli] to decode this response.",
723+
UserWarning,
724+
stacklevel=3,
725+
)
713726
continue
727+
decoders.append(decoder_cls())
714728

715729
if len(decoders) == 1:
716730
self._decoder = decoders[0]

tests/models/test_responses.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,22 @@ def test_value_error_without_request(header_value):
913913
httpx.Response(200, headers=headers, content=broken_compressed_body)
914914

915915

916+
def test_warns_when_brotli_support_missing(monkeypatch):
917+
monkeypatch.setattr(httpx._decoders, "BROTLI_INSTALLED", False, raising=False)
918+
monkeypatch.setattr(httpx._models, "BROTLI_INSTALLED", False, raising=False)
919+
if "br" in httpx._decoders.SUPPORTED_DECODERS:
920+
monkeypatch.delitem(httpx._decoders.SUPPORTED_DECODERS, "br", raising=False)
921+
922+
with pytest.warns(UserWarning, match="Content-Encoding: br"):
923+
response = httpx.Response(
924+
200,
925+
headers={"Content-Encoding": "br"},
926+
content=b"brotli-payload",
927+
)
928+
929+
assert response.content == b"brotli-payload"
930+
931+
916932
def test_response_with_unset_request():
917933
response = httpx.Response(200, content=b"Hello, world!")
918934

0 commit comments

Comments
 (0)