Skip to content
Merged
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 CHANGES/12953.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed the ``sock_read`` timeout being re-armed on a keep-alive connection after
it had been returned to the pool. An idle pooled connection could be left with a
pending read timeout that fired and poisoned it, so the next request reusing the
connection failed immediately with :exc:`aiohttp.SocketTimeoutError`. The read
timeout is now only rescheduled when resuming a transport that was actually
paused -- by :user:`daragok`.
1 change: 1 addition & 0 deletions CHANGES/12954.bugfix.rst
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ Sebastian Hanula
Sebastian Hüther
Sebastien Geffroy
SeongSoo Cho
Sergei Grachev
Sergey Ninua
Sergey Skripnick
Serhii Charykov
Expand Down
4 changes: 3 additions & 1 deletion aiohttp/client_proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,10 @@ def pause_reading(self) -> None:
self._drop_timeout()

def resume_reading(self, resume_parser: bool = True) -> None:
was_paused = self._reading_paused
super().resume_reading(resume_parser)
self._reschedule_timeout()
if was_paused:
self._reschedule_timeout()

def set_exception(
self,
Expand Down
37 changes: 37 additions & 0 deletions tests/test_client_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,43 @@ async def handler(request: web.Request) -> web.Response:
assert result == b"foo"


async def test_sock_read_timeout_not_rearmed_on_pooled_connection(
aiohttp_client: AiohttpClient,
) -> None:
# Reading the buffered body of a completed response must not re-arm the
# sock_read timeout on a connection that has already been released to the
# keep-alive pool. Otherwise the timer fires while the connection sits idle
# in the pool, stamps SocketTimeoutError on it, and the next request that
# reuses it fails immediately (with no real read having stalled).
async def handler(request: web.Request) -> web.Response:
return web.json_response({"ok": True})

app = web.Application()
app.router.add_get("/", handler)

timeout = aiohttp.ClientTimeout(total=30, sock_read=0.1)
client = await aiohttp_client(app, timeout=timeout)

async with client.get("/") as resp:
assert resp.status == 200
await resp.read()

assert client.session.connector is not None
pooled = next(iter(client.session.connector._conns.values()))
proto = pooled[0][0]
# The pooled connection must carry no read-timeout handle, otherwise
# it could trigger an exception on the next request.
assert proto._read_timeout_handle is None
assert proto.exception() is None

# The connection is still reusable.
async with client.get("/") as resp:
assert resp.status == 200
assert await resp.json() == {"ok": True}

assert next(iter(client.session.connector._conns.values()))[0][0] is proto


async def test_request_exception_cleanup_with_no_total_timeout(
aiohttp_client: AiohttpClient,
) -> None:
Expand Down
Loading