Skip to content

Commit a929c06

Browse files
authored
Fix handling of chunked+gzipped response when first chunk does not give uncompressed data (#3477) (#3485)
1 parent 10a9295 commit a929c06

File tree

3 files changed

+55
-8
lines changed

3 files changed

+55
-8
lines changed

CHANGES/3477.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix handling of chunked+gzipped response when first chunk does not give uncompressed data

aiohttp/streams.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -248,21 +248,38 @@ def feed_data(self, data: bytes, size: int=0) -> None:
248248

249249
def begin_http_chunk_receiving(self) -> None:
250250
if self._http_chunk_splits is None:
251+
if self.total_bytes:
252+
raise RuntimeError("Called begin_http_chunk_receiving when"
253+
"some data was already fed")
251254
self._http_chunk_splits = []
252255

253256
def end_http_chunk_receiving(self) -> None:
254257
if self._http_chunk_splits is None:
255258
raise RuntimeError("Called end_chunk_receiving without calling "
256259
"begin_chunk_receiving first")
257-
if not self._http_chunk_splits or \
258-
self._http_chunk_splits[-1] != self.total_bytes:
259-
self._http_chunk_splits.append(self.total_bytes)
260260

261-
# wake up readchunk when end of http chunk received
262-
waiter = self._waiter
263-
if waiter is not None:
264-
self._waiter = None
265-
set_result(waiter, False)
261+
# self._http_chunk_splits contains logical byte offsets from start of
262+
# the body transfer. Each offset is the offset of the end of a chunk.
263+
# "Logical" means bytes, accessible for a user.
264+
# If no chunks containig logical data were received, current position
265+
# is difinitely zero.
266+
pos = self._http_chunk_splits[-1] if self._http_chunk_splits else 0
267+
268+
if self.total_bytes == pos:
269+
# We should not add empty chunks here. So we check for that.
270+
# Note, when chunked + gzip is used, we can receive a chunk
271+
# of compressed data, but that data may not be enough for gzip FSM
272+
# to yield any uncompressed data. That's why current position may
273+
# not change after receiving a chunk.
274+
return
275+
276+
self._http_chunk_splits.append(self.total_bytes)
277+
278+
# wake up readchunk when end of http chunk received
279+
waiter = self._waiter
280+
if waiter is not None:
281+
self._waiter = None
282+
set_result(waiter, False)
266283

267284
async def _wait(self, func_name: str) -> None:
268285
# StreamReader uses a future to link the protocol feed_data() method

tests/test_streams.py

+29
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,35 @@ async def test_readchunk_with_other_read_calls(self) -> None:
719719
assert b'' == data
720720
assert not end_of_chunk
721721

722+
async def test_read_empty_chunks(self) -> None:
723+
"""Test that feeding empty chunks does not break stream"""
724+
stream = self._make_one()
725+
726+
# Simulate empty first chunk. This is significant special case
727+
stream.begin_http_chunk_receiving()
728+
stream.end_http_chunk_receiving()
729+
730+
stream.begin_http_chunk_receiving()
731+
stream.feed_data(b'ungzipped')
732+
stream.end_http_chunk_receiving()
733+
734+
# Possible when compression is enabled.
735+
stream.begin_http_chunk_receiving()
736+
stream.end_http_chunk_receiving()
737+
738+
# is also possible
739+
stream.begin_http_chunk_receiving()
740+
stream.end_http_chunk_receiving()
741+
742+
stream.begin_http_chunk_receiving()
743+
stream.feed_data(b' data')
744+
stream.end_http_chunk_receiving()
745+
746+
stream.feed_eof()
747+
748+
data = await stream.read()
749+
assert data == b'ungzipped data'
750+
722751
async def test_readchunk_separate_http_chunk_tail(self) -> None:
723752
"""Test that stream.readchunk returns (b'', True) when end of
724753
http chunk received after body

0 commit comments

Comments
 (0)