Don't re-arm read timeout on a connection returned to the pool - #12954
Conversation
ResponseHandler.resume_reading() rescheduled the sock_read timeout unconditionally. Reading the buffered body of a completed response calls resume_reading() after the connection has already been released to the keep-alive pool, which armed a read-timeout timer on the idle pooled connection. The timer later fired, stamped a SocketTimeoutError on the connection, and the next request that reused it failed immediately. Only reschedule the read timeout when genuinely resuming a transport that was paused for backpressure; the in-flight read timeout is armed via start_timeout() and refreshed in data_received(), so this is the only path that needed guarding. Restores the 3.13.5 behavior while keeping the parser-resume needed by the decompression-continuation path. Closes aio-libs#12953
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #12954 +/- ##
=======================================
Coverage 98.95% 98.95%
=======================================
Files 131 131
Lines 47998 48019 +21
Branches 2494 2495 +1
=======================================
+ Hits 47498 47519 +21
Misses 376 376
Partials 124 124
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
The doc-spelling job rejects 'backpressure' (not in the docs wordlist). Also remove the verbose comment in resume_reading() to match the project's comment style.
Merging this PR will not alter performance
Comparing Footnotes
|
|
@aiolibsbot review |
PR Review — Don't re-arm read timeout on a connection returned to the poolCorrect, minimal fix that restores pre-3.14 read-timeout semantics. Merge-ready.
🟢 Suggestions1. Fix is correct and minimal (`aiohttp/client_proto.py`, L198-201)The guard is right. Confirmed the bug path: after a payload reaches EOF, No change needed. 2. Test leans on real wall-clock sleep (`tests/test_client_functional.py`, L1290-1293)The assertion at line 1284 ( The trailing Checklist
Automated review by Kōan (Claude) |
|
Well, I beat the bot to it... |
Backport to 3.14: 💔 cherry-picking failed — conflicts found❌ Failed to cleanly apply 5c293f4 on top of patchback/backports/3.14/5c293f4f71f6188b446afd331afa47262a874f4f/pr-12954 Backporting merged PR #12954 into master
🤖 @patchback |
Backport to 3.15: 💔 cherry-picking failed — conflicts found❌ Failed to cleanly apply 5c293f4 on top of patchback/backports/3.15/5c293f4f71f6188b446afd331afa47262a874f4f/pr-12954 Backporting merged PR #12954 into master
🤖 @patchback |
|
Could you handle the above backports? |
|
@Dreamsorcerer, hey, thanks for the merge 🤗 I could handle the backports, if that's a question for me. Do you want me to create 2 PRs for those? to the 3.14 and 3.15 branches |
…imeouts aiohttp 3.14.0 and 3.14.1 re-arm the sock_read timer on a keep-alive connection after it has already been returned to the idle pool. The stray timer stamps a SocketTimeoutError on the pooled connection without closing it, so the pool keeps handing it out and the next request to pick it up fails instantly on an error left behind by an earlier, unrelated request. Because a single pool is shared across providers, the failures appear simultaneously across Vertex AI, Bedrock, Anthropic and OpenAI-compatible deployments as sub-millisecond "Connection timed out" errors. uv.lock resolved aiohttp 3.14.1 and the published images install via `uv sync --frozen`, so every image built from that lock shipped the regression. The wheel's own metadata declared `aiohttp>=3.10,<4.0`, which also left pip consumers free to resolve into the same broken window, so both the runtime floor and the uv constraint move to >=3.14.2. Upstream fixed this in aio-libs/aiohttp#12954, released in aiohttp 3.14.2; the lock now resolves 3.14.3. Raising the floor rather than capping below 3.14 keeps the advisories that the existing 3.14.1 floor cleared, so no osv-scanner ignores are needed. litellm requires Python >=3.10 and aiohttp 3.14.2 requires >=3.10, so no supported interpreter loses support. Both new tests fail on the previous pins and pass on these.
…imeouts aiohttp 3.14.0 and 3.14.1 re-arm the sock_read timer on a keep-alive connection after it has already been returned to the idle pool. The stray timer stamps a SocketTimeoutError on the pooled connection without closing it, so the pool keeps handing it out and the next request to pick it up fails instantly on an error left behind by an earlier, unrelated request. Because a single pool is shared across providers, the failures appear simultaneously across Vertex AI, Bedrock, Anthropic and OpenAI-compatible deployments as sub-millisecond "Connection timed out" errors. uv.lock resolved aiohttp 3.14.1 and the published images install via `uv sync --frozen`, so every image built from that lock shipped the regression. The wheel's own metadata declared `aiohttp>=3.10,<4.0`, which also left pip consumers free to resolve into the same broken window, so both the runtime floor and the uv constraint move to >=3.14.2. Upstream fixed this in aio-libs/aiohttp#12954, released in aiohttp 3.14.2; the lock now resolves 3.14.3. Raising the floor rather than capping below 3.14 keeps the advisories that the existing 3.14.1 floor cleared, so no osv-scanner ignores are needed. litellm requires Python >=3.10 and aiohttp 3.14.2 requires >=3.10, so no supported interpreter loses support. Both new tests fail on the previous pins and pass on these. (cherry picked from commit ffd6ac5)
What do these changes do?
ResponseHandler.resume_reading()rescheduled thesock_readread timeout on every call. Butresume_reading()is also invoked while draining an already-buffered, completed response (StreamReader._read_nowait_chunk/feed_eof), which happens after the connection has been released to the keep-alive pool. That armed asock_readtimer on an idle pooled connection; the timer later fired, stamped aSocketTimeoutErroron the connection via_on_read_timeout, and the next request that reused the connection failed immediately (DataQueue.read()→raise self._exception), long before any realsock_readwindow.The fix only reschedules the read timeout when genuinely resuming a transport that was paused for backpressure (a read is still in flight). The in-flight read timeout is otherwise armed by
start_timeout()and refreshed indata_received(), so thisresume_reading()path was the only one that needed guarding. The parser-resume (data_received(b"")) used by the decompression-continuation path (#11966) is preserved.This restores the 3.13.5 behavior (where
resume_reading()only ran when the transport was actually paused).Are there changes in behavior for the user?
Yes — a regression introduced in 3.14 is fixed. Clients that set
ClientTimeout(sock_read=...)together with the keep-alive pool no longer get pooled connections poisoned by a stray read timeout, so reused connections stop failing with an immediateSocketTimeoutError. The exposure is worst whenkeepalive_timeout > sock_read. No public API change.Is it a substantial burden for the maintainers to support this?
No — the change is three lines, with a regression test. It narrows when the read timeout is rescheduled to match pre-3.14 semantics.
Related issue number
Fixes #12953
Checklist
CONTRIBUTORS.txtCHANGES/folderTest logs
Drafted with Claude Code (Opus 4.8); reviewed by @daragok.