From e49977c9b96dd5b7291989c5adf480c6fb37519c Mon Sep 17 00:00:00 2001 From: availov Date: Wed, 1 Apr 2026 03:20:23 +0300 Subject: [PATCH 1/7] Fix spurious 'Future exception was never retrieved' warning on disconnect during backpressure --- CHANGES/12281.bugfix.rst | 3 +++ aiohttp/base_protocol.py | 2 +- tests/test_web_server.py | 57 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12281.bugfix.rst diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst new file mode 100644 index 00000000000..4eff61e0b5a --- /dev/null +++ b/CHANGES/12281.bugfix.rst @@ -0,0 +1,3 @@ +Fixed spurious ``Future exception was never retrieved`` warnings logged by +the asyncio event loop when a client disconnected while the server write +buffer was paused due to TCP back-pressure -- by :user:`availov`. diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py index 7f01830f4e9..d7d83425b88 100644 --- a/aiohttp/base_protocol.py +++ b/aiohttp/base_protocol.py @@ -97,4 +97,4 @@ async def _drain_helper(self) -> None: if waiter is None: waiter = self._loop.create_future() self._drain_waiter = waiter - await asyncio.shield(waiter) + await waiter diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 2cd364e0317..0174c61e6c4 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -8,7 +8,7 @@ from aiohttp import client, web from aiohttp.http_exceptions import BadHttpMethod, BadStatusLine -from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer +from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer, AiohttpServer async def test_simple_server( @@ -454,3 +454,58 @@ async def on_request(request: web.Request) -> web.Response: assert done_event.is_set() finally: await asyncio.gather(runner.shutdown(), site.stop()) + + +async def test_no_future_warning_on_disconnect_during_backpressure( + aiohttp_server: AiohttpServer, +) -> None: + import gc + + loop = asyncio.get_event_loop() + exc_handler_calls: list[dict] = [] + original_handler = loop.get_exception_handler() + loop.set_exception_handler(lambda _loop, ctx: exc_handler_calls.append(ctx)) + + protocol_holder: list[web.RequestHandler] = [] + + async def handler(request: web.Request) -> NoReturn: + protocol_holder.append(request.protocol) # type: ignore[arg-type] + resp = web.StreamResponse() + await resp.prepare(request) + while True: + await resp.write(b"x" * 65536) + + app = web.Application() + app.router.add_route("GET", "/", handler) + # aiohttp_server enables handler_cancellation by default so the handler + # task is cancelled when connection_lost() fires. + server = await aiohttp_server(app) + + # Open a raw asyncio connection so we control exactly when the client + # side closes. + reader, writer = await asyncio.open_connection(server.host, server.port) + writer.write(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + await writer.drain() + + try: + # Poll until the server protocol reports that writing is paused. + async def wait_for_backpressure() -> None: + while not protocol_holder or not protocol_holder[0].writing_paused: + await asyncio.sleep(0.01) + + await asyncio.wait_for(wait_for_backpressure(), timeout=5.0) + + writer.close() + await asyncio.sleep(0.1) + + gc.collect() + await asyncio.sleep(0) + finally: + loop.set_exception_handler(original_handler) + + orphan_warnings = [ + ctx + for ctx in exc_handler_calls + if "exception was never retrieved" in ctx.get("message", "").lower() + ] + assert not orphan_warnings, f"Unexpected asyncio warnings: {orphan_warnings}" From 92e3a496435ef90d425feb291f25712d494e6119 Mon Sep 17 00:00:00 2001 From: availov Date: Wed, 1 Apr 2026 03:48:33 +0300 Subject: [PATCH 2/7] Add Yury Novikov to CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4a3934e7df7..e61c5e8e328 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -420,6 +420,7 @@ Yegor Roganov Yifei Kong Young-Ho Cha Yuriy Shatrov +Yury Novikov Yury Pliner Yury Selivanov Yusuke Tsutsumi From 7164789fcacf2cdc7cb66cb169abc64f1b7d4ce8 Mon Sep 17 00:00:00 2001 From: availov Date: Wed, 1 Apr 2026 03:55:59 +0300 Subject: [PATCH 3/7] Update type hints in test_web_server.py --- tests/test_web_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 0174c61e6c4..5941b070c13 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -1,7 +1,7 @@ import asyncio import socket from contextlib import suppress -from typing import NoReturn +from typing import Any, NoReturn from unittest import mock import pytest @@ -462,14 +462,14 @@ async def test_no_future_warning_on_disconnect_during_backpressure( import gc loop = asyncio.get_event_loop() - exc_handler_calls: list[dict] = [] + exc_handler_calls: list[dict[str, Any]] = [] original_handler = loop.get_exception_handler() loop.set_exception_handler(lambda _loop, ctx: exc_handler_calls.append(ctx)) - protocol_holder: list[web.RequestHandler] = [] + protocol_holder: list[web.RequestHandler[web.Request]] = [] async def handler(request: web.Request) -> NoReturn: - protocol_holder.append(request.protocol) # type: ignore[arg-type] + protocol_holder.append(request.protocol) resp = web.StreamResponse() await resp.prepare(request) while True: From c4fc72d9d2bd7646c91e1fdd6b3e99e51e6e9771 Mon Sep 17 00:00:00 2001 From: availov Date: Wed, 1 Apr 2026 04:11:56 +0300 Subject: [PATCH 4/7] Fix spurious 'Future exception was never retrieved' warning on disconnect during backpressure --- CHANGES/12281.bugfix.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst index 4eff61e0b5a..41a8644e80b 100644 --- a/CHANGES/12281.bugfix.rst +++ b/CHANGES/12281.bugfix.rst @@ -1,3 +1 @@ -Fixed spurious ``Future exception was never retrieved`` warnings logged by -the asyncio event loop when a client disconnected while the server write -buffer was paused due to TCP back-pressure -- by :user:`availov`. +Fixed spurious ``Future exception was never retrieved`` warning on disconnect during backpressure -- by :user:`availov`. From e77bc3107fafa558bf8ad24754971af98ac880ee Mon Sep 17 00:00:00 2001 From: availov Date: Wed, 1 Apr 2026 04:21:38 +0300 Subject: [PATCH 5/7] Fix spurious 'Future exception was never retrieved' warning on disconnect during back-pressure --- CHANGES/12281.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst index 41a8644e80b..63521a73b1c 100644 --- a/CHANGES/12281.bugfix.rst +++ b/CHANGES/12281.bugfix.rst @@ -1 +1 @@ -Fixed spurious ``Future exception was never retrieved`` warning on disconnect during backpressure -- by :user:`availov`. +Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`. From 5e28940abb8082f8e44815419bf09f312fb1f38c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 3 Apr 2026 01:12:32 +0100 Subject: [PATCH 6/7] Tweak test --- tests/test_web_server.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 5941b070c13..0afc86a2556 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -1,4 +1,5 @@ import asyncio +import gc import socket from contextlib import suppress from typing import Any, NoReturn @@ -459,17 +460,15 @@ async def on_request(request: web.Request) -> web.Response: async def test_no_future_warning_on_disconnect_during_backpressure( aiohttp_server: AiohttpServer, ) -> None: - import gc - - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() exc_handler_calls: list[dict[str, Any]] = [] original_handler = loop.get_exception_handler() loop.set_exception_handler(lambda _loop, ctx: exc_handler_calls.append(ctx)) - - protocol_holder: list[web.RequestHandler[web.Request]] = [] + protocol = None async def handler(request: web.Request) -> NoReturn: - protocol_holder.append(request.protocol) + nonlocal protocol + protocol = request.protocol resp = web.StreamResponse() await resp.prepare(request) while True: @@ -503,9 +502,4 @@ async def wait_for_backpressure() -> None: finally: loop.set_exception_handler(original_handler) - orphan_warnings = [ - ctx - for ctx in exc_handler_calls - if "exception was never retrieved" in ctx.get("message", "").lower() - ] - assert not orphan_warnings, f"Unexpected asyncio warnings: {orphan_warnings}" + assert not exc_handler_calls From 00e7e6c1e764ac6ca2e4c51b8e83bbd697058daf Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 3 Apr 2026 01:16:30 +0100 Subject: [PATCH 7/7] Update test_web_server.py --- tests/test_web_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 0afc86a2556..63f27f01b25 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -489,7 +489,7 @@ async def handler(request: web.Request) -> NoReturn: try: # Poll until the server protocol reports that writing is paused. async def wait_for_backpressure() -> None: - while not protocol_holder or not protocol_holder[0].writing_paused: + while protocol is None or not protocol.writing_paused: await asyncio.sleep(0.01) await asyncio.wait_for(wait_for_backpressure(), timeout=5.0)