diff --git a/src/httpx2/CHANGELOG.md b/src/httpx2/CHANGELOG.md index 51e24cee..62c1fdad 100644 --- a/src/httpx2/CHANGELOG.md +++ b/src/httpx2/CHANGELOG.md @@ -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 diff --git a/src/httpx2/httpx2/_decoders.py b/src/httpx2/httpx2/_decoders.py index b8b1bb29..1087a906 100644 --- a/src/httpx2/httpx2/_decoders.py +++ b/src/httpx2/httpx2/_decoders.py @@ -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)) diff --git a/src/httpx2/httpx2/_models.py b/src/httpx2/httpx2/_models.py index 834f0806..314342bd 100644 --- a/src/httpx2/httpx2/_models.py +++ b/src/httpx2/httpx2/_models.py @@ -12,7 +12,6 @@ from ._content import ByteStream, UnattachedStream, encode_request, encode_response from ._decoders import ( - SUPPORTED_DECODERS, ByteChunker, ContentDecoder, IdentityDecoder, @@ -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() diff --git a/tests/httpx2/test_decoders.py b/tests/httpx2/test_decoders.py index 2f137f7f..93b11c82 100644 --- a/tests/httpx2/test_decoders.py +++ b/tests/httpx2/test_decoders.py @@ -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"