Skip to content

Commit 0398aa1

Browse files
miss-islingtonasvetlovclaude
authored
[3.15] gh-93821: Keep asyncio proactor servers accepting after a connection reset (GH-161238) (#161308)
gh-93821: Keep asyncio proactor servers accepting after a connection reset (GH-161238) (cherry picked from commit f88565b) Co-authored-by: Andrew Svetlov <andrew.svetlov@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b2247bb commit 0398aa1

5 files changed

Lines changed: 76 additions & 1 deletion

File tree

‎Lib/asyncio/proactor_events.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,13 @@ def loop(f=None):
892892
if self.is_closed():
893893
return
894894
f = self._proactor.accept(sock)
895+
except (ConnectionResetError, ConnectionAbortedError):
896+
# The peer closed the connection before it was accepted.
897+
# The listening socket is still usable: keep accepting.
898+
if self._debug:
899+
logger.debug("Accept failed on socket %r",
900+
sock, exc_info=True)
901+
self.call_soon(loop)
895902
except OSError as exc:
896903
if sock.fileno() != -1:
897904
self.call_exception_handler({

‎Lib/asyncio/windows_events.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,14 @@ def accept(self, listener):
546546
ov.AcceptEx(listener.fileno(), conn.fileno())
547547

548548
def finish_accept(trans, key, ov):
549-
ov.getresult()
549+
try:
550+
ov.getresult()
551+
except OSError as exc:
552+
conn.close()
553+
# The peer reset the connection before it was accepted.
554+
if exc.winerror == _overlapped.ERROR_NETNAME_DELETED:
555+
raise ConnectionResetError(*exc.args)
556+
raise
550557
# Use SO_UPDATE_ACCEPT_CONTEXT so getsockname() etc work.
551558
buf = struct.pack('@P', listener.fileno())
552559
conn.setsockopt(socket.SOL_SOCKET,
@@ -561,6 +568,9 @@ async def accept_coro(future, conn):
561568
except exceptions.CancelledError:
562569
conn.close()
563570
raise
571+
except ConnectionResetError:
572+
# Reported to the caller through the future.
573+
pass
564574

565575
future = self._register(ov, listener, finish_accept)
566576
coro = accept_coro(future, conn)

‎Lib/test/test_asyncio/test_proactor_events.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,26 @@ def test_create_server(self, m_log):
868868
self.assertTrue(self.sock.close.called)
869869
self.assertTrue(m_log.error.called)
870870

871+
def test_create_server_connection_reset(self):
872+
# gh-93821: a peer resetting its connection before it is accepted
873+
# must not close the listening socket.
874+
pf = mock.Mock()
875+
call_soon = self.loop.call_soon = mock.Mock()
876+
self.loop.call_exception_handler = mock.Mock()
877+
878+
self.loop._start_serving(pf, self.sock)
879+
loop = call_soon.call_args[0][0]
880+
881+
for exc in ConnectionResetError, ConnectionAbortedError:
882+
with self.subTest(exc=exc):
883+
call_soon.reset_mock()
884+
fut = mock.Mock()
885+
fut.result.side_effect = exc()
886+
loop(fut)
887+
self.assertFalse(self.sock.close.called)
888+
self.assertFalse(self.loop.call_exception_handler.called)
889+
call_soon.assert_called_once_with(loop)
890+
871891
def test_create_server_cancel(self):
872892
pf = mock.Mock()
873893
call_soon = self.loop.call_soon = mock.Mock()

‎Lib/test/test_asyncio/test_windows_events.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import signal
33
import socket
4+
import struct
45
import sys
56
import time
67
import threading
@@ -19,6 +20,7 @@
1920
from asyncio import windows_utils
2021
from test import support
2122
from test.support import os_helper
23+
from test.support import socket_helper
2224
from test.test_asyncio import utils as test_utils
2325

2426

@@ -115,6 +117,38 @@ def test_close(self):
115117
self.assertEqual(f.result(), b'')
116118
b.close()
117119

120+
def test_accept_connection_reset(self):
121+
# gh-93821: a connection reset while it waits in the listen queue
122+
# makes AcceptEx fail with ERROR_NETNAME_DELETED. The server must
123+
# keep accepting.
124+
async def handle(reader, writer):
125+
writer.close()
126+
127+
async def main():
128+
server = await asyncio.start_server(handle, socket_helper.HOSTv4, 0)
129+
port = server.sockets[0].getsockname()[1]
130+
# Both connects complete while the loop is blocked here: the
131+
# first is taken by the pending AcceptEx, the second waits in
132+
# the listen queue and is reset there.
133+
first = socket.create_connection((socket_helper.HOSTv4, port))
134+
second = socket.create_connection((socket_helper.HOSTv4, port))
135+
second.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
136+
struct.pack('HH', 1, 0))
137+
second.close()
138+
try:
139+
await asyncio.sleep(0.5)
140+
reader, writer = await asyncio.wait_for(
141+
asyncio.open_connection(socket_helper.HOSTv4, port),
142+
support.SHORT_TIMEOUT)
143+
writer.close()
144+
await writer.wait_closed()
145+
finally:
146+
first.close()
147+
server.close()
148+
await server.wait_closed()
149+
150+
self.loop.run_until_complete(main())
151+
118152
def test_double_bind(self):
119153
ADDRESS = r'\\.\pipe\test_double_bind-%s' % os.getpid()
120154
server1 = windows_events.PipeServer(ADDRESS)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :class:`asyncio.ProactorEventLoop` servers that stopped accepting new
2+
connections on Windows when a client reset its connection before it was
3+
accepted (``WinError 64``, ``ERROR_NETNAME_DELETED``). The listening socket is
4+
now kept open and the server continues to accept connections.

0 commit comments

Comments
 (0)