Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/httpx2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Changed

* Limit the number of chained `Content-Encoding` decoders to 5.

## 2.3.0 (June 1st, 2026)

### Changed
Expand Down
9 changes: 7 additions & 2 deletions src/httpx2/httpx2/_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,16 @@ class MultiDecoder(ContentDecoder):
Handle the case where multiple encodings have been applied.
"""

def __init__(self, children: typing.Sequence[ContentDecoder]) -> None:
max_decode_links: typing.ClassVar[int] = 5

def __init__(self, encodings: typing.Sequence[str]) -> None:
"""
'children' should be a sequence of decoders in the order in which
'encodings' should be the content codings in the order in which
each was applied.
"""
if len(encodings) > self.max_decode_links:
raise DecodingError(f"Cannot apply more than {self.max_decode_links} content encodings.")
children = [SUPPORTED_DECODERS[encoding]() for encoding in encodings if encoding in SUPPORTED_DECODERS]
# Note that we reverse the order for decoding.
self.children = list(reversed(children))

Expand Down
20 changes: 6 additions & 14 deletions src/httpx2/httpx2/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from ._content import ByteStream, UnattachedStream, encode_request, encode_response
from ._decoders import (
SUPPORTED_DECODERS,
ByteChunker,
ContentDecoder,
IdentityDecoder,
Expand Down Expand Up @@ -679,20 +678,13 @@ def _get_content_decoder(self) -> ContentDecoder:
content, depending on the Content-Encoding used in the response.
"""
if not hasattr(self, "_decoder"):
decoders: list[ContentDecoder] = []
values = self.headers.get_list("content-encoding", split_commas=True)
for value in values:
value = value.strip().lower()
try:
decoder_cls = SUPPORTED_DECODERS[value]
decoders.append(decoder_cls())
except KeyError:
continue

if len(decoders) == 1:
self._decoder = decoders[0]
elif len(decoders) > 1:
self._decoder = MultiDecoder(children=decoders)
encodings = [value.strip().lower() for value in values]
decoder = MultiDecoder(encodings)
if len(decoder.children) == 1:
self._decoder = decoder.children[0]
elif decoder.children:
self._decoder = decoder
else:
self._decoder = IdentityDecoder()

Expand Down
6 changes: 6 additions & 0 deletions tests/httpx2/test_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ def test_multi_with_identity() -> None:
assert response.content == body


def test_multi_decode_links_limit() -> None:
headers = [(b"Content-Encoding", b", ".join([b"gzip"] * 6))]
with pytest.raises(httpx2.DecodingError, match="Cannot apply more than 5 content encodings"):
httpx2.Response(200, headers=headers, content=b"")


@pytest.mark.anyio
async def test_streaming() -> None:
body = b"test 123"
Expand Down
Loading