https://github.com/python/cpython/commit/788c3291172b55efa7cf8b33a315e4b0fe63540c
commit: 788c3291172b55efa7cf8b33a315e4b0fe63540c
branch: main
author: Guido van Rossum <[email protected]>
committer: gvanrossum <[email protected]>
date: 2026-03-14T11:28:49-07:00
summary:

gh-123720: When closing an asyncio server, stop the handlers (#124689)

files:
A Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst
M Lib/asyncio/base_events.py
M Lib/test/test_asyncio/test_server.py

diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py
index 0930ef403c6c4b..77c70aaa7b986e 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
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):
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..04e6a377dd816c
--- /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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to