From 6f9523056515268b7bd70fc766b6704f186591e1 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 27 Sep 2024 10:41:44 -0700 Subject: [PATCH 1/4] When closing an asyncio server, stop the handlers --- Lib/asyncio/base_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index ffcc0174e1e245..a42c4ec003718b 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -381,6 +381,7 @@ async def serve_forever(self): except exceptions.CancelledError: try: self.close() + self.close_clients() await self.wait_closed() finally: raise From cadc7516068dc12273fafb4aa835191781c25385 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 13 Mar 2026 16:35:03 -0700 Subject: [PATCH 2/4] Add NEWS file --- .../Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst new file mode 100644 index 00000000000000..17faa79b65e4f4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst @@ -0,0 +1,5 @@ +asyncio: Fix :func:`asyncio.Server.serve_forever` shutdown regression. Since +3.12, cancelling `serve_forever()` could hang waiting for a handler blocked +on a read from a client that never closed (effectively requiring two +interrupts to stop); the shutdown sequence now ensures client streams are +closed so `serve_forever()` exits promptly and handlers observe EOF. From 6c66c434086f60409eff6ac941d721dc8c02f4ea Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 13 Mar 2026 16:57:08 -0700 Subject: [PATCH 3/4] Add test (by ordinary-jamie) --- Lib/test/test_asyncio/test_server.py | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index 5bd0f7e2af4f84..581ea47d2dec97 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -266,6 +266,38 @@ async def serve(rd, wr): await asyncio.sleep(0) self.assertTrue(task.done()) + async def test_close_with_hanging_client(self): + # Synchronize server cancellation only after the socket connection is + # accepted and this event is set + conn_event = asyncio.Event() + class Proto(asyncio.Protocol): + def connection_made(self, transport): + conn_event.set() + + loop = asyncio.get_running_loop() + srv = await loop.create_server(Proto, socket_helper.HOSTv4, 0) + + # Start the server + serve_forever_task = asyncio.create_task(srv.serve_forever()) + await asyncio.sleep(0) + + # Create a connection to server + addr = srv.sockets[0].getsockname() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(addr) + self.addCleanup(sock.close) + + # Send a CancelledError into the server to emulate a Ctrl+C + # KeyboardInterrupt whilst the server is handling a hanging client + await conn_event.wait() + serve_forever_task.cancel() + + # Ensure the client is closed within a timeout + async with asyncio.timeout(0.5): + await srv.wait_closed() + + self.assertFalse(srv.is_serving()) + # Test the various corner cases of Unix server socket removal class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase): From 56e4b1aaf10c04ba697e0b029039d8047d39e62b Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 13 Mar 2026 17:01:34 -0700 Subject: [PATCH 4/4] Use double backticks in NEWS file --- .../Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst index 17faa79b65e4f4..04e6a377dd816c 100644 --- a/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst +++ b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst @@ -1,5 +1,5 @@ asyncio: Fix :func:`asyncio.Server.serve_forever` shutdown regression. Since -3.12, cancelling `serve_forever()` could hang waiting for a handler blocked +3.12, cancelling ``serve_forever()`` could hang waiting for a handler blocked on a read from a client that never closed (effectively requiring two interrupts to stop); the shutdown sequence now ensures client streams are -closed so `serve_forever()` exits promptly and handlers observe EOF. +closed so ``serve_forever()`` exits promptly and handlers observe EOF.