Skip to content

Commit 53f0eeb

Browse files
asvetlovmjpieters
andauthored
[3.6] Reset the sock_read timeout when data arrives (#4142) (#4143)
The sock_read timeout should apply *per read operation*, but currently applies cumulatively across all reads. Reschedule the timeout each time data is received and add tests to validate that the timeout doesn't interfere with overall reading and correctly catches reads that take too long. Co-authored-by: Martijn Pieters <[email protected]>
1 parent 67e3130 commit 53f0eeb

File tree

4 files changed

+54
-0
lines changed

4 files changed

+54
-0
lines changed

CHANGES/3808.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reset the ``sock_read`` timeout each time data is received for a ``aiohttp.client`` response.

CONTRIBUTORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ Manuel Miranda
160160
Marat Sharafutdinov
161161
Marco Paolini
162162
Mariano Anaya
163+
Martijn Pieters
163164
Martin Melka
164165
Martin Richard
165166
Mathias Fröjdman

aiohttp/client_proto.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ def _on_read_timeout(self) -> None:
176176
self._payload.set_exception(exc)
177177

178178
def data_received(self, data: bytes) -> None:
179+
self._reschedule_timeout()
180+
179181
if not data:
180182
return
181183

tests/test_client_functional.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,56 @@ async def handler(request):
598598
await client.get('/')
599599

600600

601+
async def test_read_timeout_between_chunks(aiohttp_client, mocker) -> None:
602+
mocker.patch('aiohttp.helpers.ceil').side_effect = ceil
603+
604+
async def handler(request):
605+
resp = aiohttp.web.StreamResponse()
606+
await resp.prepare(request)
607+
# write data 4 times, with pauses. Total time 0.4 seconds.
608+
for _ in range(4):
609+
await asyncio.sleep(0.1)
610+
await resp.write(b'data\n')
611+
return resp
612+
613+
app = web.Application()
614+
app.add_routes([web.get('/', handler)])
615+
616+
# A timeout of 0.2 seconds should apply per read.
617+
timeout = aiohttp.ClientTimeout(sock_read=0.2)
618+
client = await aiohttp_client(app, timeout=timeout)
619+
620+
res = b''
621+
async with await client.get('/') as resp:
622+
res += await resp.read()
623+
624+
assert res == b'data\n' * 4
625+
626+
627+
async def test_read_timeout_on_reading_chunks(aiohttp_client, mocker) -> None:
628+
mocker.patch('aiohttp.helpers.ceil').side_effect = ceil
629+
630+
async def handler(request):
631+
resp = aiohttp.web.StreamResponse()
632+
await resp.prepare(request)
633+
await resp.write(b'data\n')
634+
await asyncio.sleep(1)
635+
await resp.write(b'data\n')
636+
return resp
637+
638+
app = web.Application()
639+
app.add_routes([web.get('/', handler)])
640+
641+
# A timeout of 0.2 seconds should apply per read.
642+
timeout = aiohttp.ClientTimeout(sock_read=0.2)
643+
client = await aiohttp_client(app, timeout=timeout)
644+
645+
async with await client.get('/') as resp:
646+
assert (await resp.content.read(5)) == b'data\n'
647+
with pytest.raises(asyncio.TimeoutError):
648+
await resp.content.read()
649+
650+
601651
async def test_timeout_on_reading_data(aiohttp_client, mocker) -> None:
602652
loop = asyncio.get_event_loop()
603653

0 commit comments

Comments
 (0)