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]