Skip to content

Commit 571a9b7

Browse files
illia-vCycloctane
andauthored
Fix HTTPResponse.read_chunked when leftover data is present in decoder's buffer (#3736)
* Fix `HTTPResponse.read_chunked`when data is present in decoder's buffer * Simplify solution based on #3735 Co-authored-by: Rui Xi <[email protected]> --------- Co-authored-by: Rui Xi <[email protected]>
1 parent bfe8e19 commit 571a9b7

3 files changed

Lines changed: 51 additions & 4 deletions

File tree

changelog/3734.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed ``HTTPResponse.read_chunked()`` to properly handle leftover data in
2+
the decoder's buffer when reading compressed chunked responses.

src/urllib3/response.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,10 +1407,14 @@ def read_chunked(
14071407
amt = None
14081408

14091409
while True:
1410-
self._update_chunk_length()
1411-
if self.chunk_left == 0:
1412-
break
1413-
chunk = self._handle_chunk(amt)
1410+
# First, check if any data is left in the decoder's buffer.
1411+
if self._decoder and self._decoder.has_unconsumed_tail:
1412+
chunk = b""
1413+
else:
1414+
self._update_chunk_length()
1415+
if self.chunk_left == 0:
1416+
break
1417+
chunk = self._handle_chunk(amt)
14141418
decoded = self._decode(
14151419
chunk,
14161420
decode_content=decode_content,

test/test_response.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,47 @@ def test_read_with_all_data_already_in_decompressor(
688688
assert read(0) == b""
689689
assert read() == b""
690690

691+
@pytest.mark.parametrize("read_method", ("read_chunked", "stream"))
692+
@pytest.mark.parametrize(
693+
"data",
694+
[d[1] for d in _test_compressor_params],
695+
ids=[d[0] for d in _test_compressor_params],
696+
)
697+
def test_read_chunked_with_all_data_already_in_decompressor(
698+
self,
699+
request: pytest.FixtureRequest,
700+
read_method: str,
701+
data: tuple[str, typing.Callable[[bytes], bytes]] | None,
702+
) -> None:
703+
if data is None:
704+
pytest.skip(f"Proper {request.node.callspec.id} decoder is not available")
705+
original_data = b"0" * 100_000 # 100 KB
706+
name, compress_func = data
707+
compressed_data = compress_func(original_data)
708+
httplib_r = httplib.HTTPResponse(MockSock) # type: ignore[arg-type]
709+
httplib_r.fp = MockChunkedEncodingResponse([compressed_data]) # type: ignore[assignment]
710+
r = HTTPResponse(
711+
httplib_r,
712+
preload_content=False,
713+
headers={"transfer-encoding": "chunked", "content-encoding": name},
714+
)
715+
# Mark the current chunk as fully read.
716+
r.chunk_left = 0
717+
# Feed all compressed data to the decoder.
718+
r._init_decoder()
719+
assert r._decoder is not None # for mypy
720+
initially_decoded = r._decoder.decompress(compressed_data, max_length=0)
721+
result = list(getattr(r, read_method)(amt=10240, decode_content=True))
722+
if name == "br":
723+
# It's known that some Brotli libraries do not respect
724+
# `max_length`.
725+
result = [initially_decoded] + list(result)
726+
else:
727+
assert initially_decoded == b""
728+
assert all(len(chunk) == 10240 for chunk in result[:-1])
729+
assert len(result[-1]) == len(original_data) % 10240
730+
assert b"".join(result) == original_data
731+
691732
@pytest.mark.parametrize(
692733
"delta",
693734
(

0 commit comments

Comments
 (0)