https://github.com/python/cpython/commit/482cf6b930999334794f400ff5b5cbc48df7d2d9
commit: 482cf6b930999334794f400ff5b5cbc48df7d2d9
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-21T23:24:43+05:30
summary:

[3.15] gh-156784: do not ignore error port unreachable in proactor event loop 
implementation (GH-156788) (#159872)

gh-156784: do not ignore error port unreachable in proactor event loop 
implementation (GH-156788)
(cherry picked from commit 0815fe3afc001e4cedc44cc8007ce8597eba7c51)

Co-authored-by: Thomas Grainger <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-01-00-00-00.gh-issue-156784.8gvtt4.rst
M Lib/asyncio/windows_events.py
M Lib/test/test_asyncio/test_events.py
M Lib/test/test_asyncio/test_sock_lowlevel.py

diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py
index c905db1016807f..e51a88df9e1cd7 100644
--- a/Lib/asyncio/windows_events.py
+++ b/Lib/asyncio/windows_events.py
@@ -8,7 +8,6 @@
 import _overlapped
 import _winapi
 import errno
-from functools import partial
 import math
 import msvcrt
 import socket
@@ -462,24 +461,20 @@ def finish_socket_func(trans, key, ov):
         try:
             return ov.getresult()
         except OSError as exc:
+            # ERROR_PORT_UNREACHABLE is reported by WSARecvFrom when the
+            # same socket was previously used to send to an address that
+            # isn't listening (gh-91227). Surface it as a
+            # ConnectionResetError, like the other recoverable codes here,
+            # so it propagates to the caller the same way a plain
+            # socket.recvfrom() already does on SelectorEventLoop, instead
+            # of being silently swallowed.
             if exc.winerror in (_overlapped.ERROR_NETNAME_DELETED,
-                                _overlapped.ERROR_OPERATION_ABORTED):
+                                _overlapped.ERROR_OPERATION_ABORTED,
+                                _overlapped.ERROR_PORT_UNREACHABLE):
                 raise ConnectionResetError(*exc.args)
             else:
                 raise
 
-    @classmethod
-    def _finish_recvfrom(cls, trans, key, ov, *, empty_result):
-        try:
-            return cls.finish_socket_func(trans, key, ov)
-        except OSError as exc:
-            # WSARecvFrom will report ERROR_PORT_UNREACHABLE when the same
-            # socket is used to send to an address that is not listening.
-            if exc.winerror == _overlapped.ERROR_PORT_UNREACHABLE:
-                return empty_result, None
-            else:
-                raise
-
     def recv(self, conn, nbytes, flags=0):
         self._register_with_iocp(conn)
         ov = _overlapped.Overlapped(NULL)
@@ -514,8 +509,7 @@ def recvfrom(self, conn, nbytes, flags=0):
         except BrokenPipeError:
             return self._result((b'', None))
 
-        return self._register(ov, conn, partial(self._finish_recvfrom,
-                                                empty_result=b''))
+        return self._register(ov, conn, self.finish_socket_func)
 
     def recvfrom_into(self, conn, buf, flags=0):
         self._register_with_iocp(conn)
@@ -525,8 +519,7 @@ def recvfrom_into(self, conn, buf, flags=0):
         except BrokenPipeError:
             return self._result((0, None))
 
-        return self._register(ov, conn, partial(self._finish_recvfrom,
-                                                empty_result=0))
+        return self._register(ov, conn, self.finish_socket_func)
 
     def sendto(self, conn, buf, flags=0, addr=None):
         self._register_with_iocp(conn)
diff --git a/Lib/test/test_asyncio/test_events.py 
b/Lib/test/test_asyncio/test_events.py
index 5c4136df214175..a3093d4dc8a748 100644
--- a/Lib/test/test_asyncio/test_events.py
+++ b/Lib/test/test_asyncio/test_events.py
@@ -1523,9 +1523,18 @@ class Protocol(asyncio.DatagramProtocol):
 
             _received_datagram = None
 
+            def connection_made(self, transport):
+                self.errors = []
+                self.error_received_event = loop.create_future()
+
             def datagram_received(self, data, addr):
                 self._received_datagram.set_result(data)
 
+            def error_received(self, exc):
+                self.errors.append(exc)
+                if not self.error_received_event.done():
+                    self.error_received_event.set_result(None)
+
             async def wait_for_datagram_received(self):
                 self._received_datagram = loop.create_future()
                 result = await asyncio.wait_for(self._received_datagram, 10)
@@ -1580,6 +1589,15 @@ def create_socket():
             protocol_1.wait_for_datagram_received()
         ), b'd')
 
+        if sys.platform == 'win32':
+            # The bad send to addr_3 should be surfaced to the protocol
+            # via error_received() instead of being silently dropped,
+            # while transport_1 keeps working as shown above.
+            loop.run_until_complete(
+                asyncio.wait_for(protocol_1.error_received_event, 10))
+            self.assertTrue(protocol_1.errors)
+            self.assertIsInstance(protocol_1.errors[0], ConnectionResetError)
+
         transport_1.close()
         transport_2.close()
 
diff --git a/Lib/test/test_asyncio/test_sock_lowlevel.py 
b/Lib/test/test_asyncio/test_sock_lowlevel.py
index 8380342e294712..6ce029008f1446 100644
--- a/Lib/test/test_asyncio/test_sock_lowlevel.py
+++ b/Lib/test/test_asyncio/test_sock_lowlevel.py
@@ -613,19 +613,10 @@ def test_create_connection_sock(self):
 
 if sys.platform == 'win32':
 
-    class SelectEventLoopTests(BaseSockTestsMixin,
-                               test_utils.TestCase):
-
-        def create_event_loop(self):
-            return asyncio.SelectorEventLoop()
-
-
-    class ProactorEventLoopTests(BaseSockTestsMixin,
-                                 test_utils.TestCase):
-
-        def create_event_loop(self):
-            return asyncio.ProactorEventLoop()
-
+    class _DatagramSendToNonListeningAddressMixin:
+        # Shared by SelectEventLoopTests and ProactorEventLoopTests so that
+        # sock_recvfrom()/sock_recvfrom_into() behave identically on both
+        # event loop implementations.
 
         async def _basetest_datagram_send_to_non_listening_address(self,
                                                                    recvfrom):
@@ -634,8 +625,9 @@ async def 
_basetest_datagram_send_to_non_listening_address(self,
             #   https://github.com/python/cpython/issues/88906
             #   https://bugs.python.org/issue47071
             #   https://bugs.python.org/issue44743
-            # The Proactor event loop would fail to receive datagram messages
-            # after sending a message to an address that wasn't listening.
+            # Sending a datagram to an address that isn't listening can
+            # surface as a ConnectionResetError on a later receive; the
+            # socket must still be usable afterwards.
 
             def create_socket():
                 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -670,7 +662,8 @@ def create_socket():
 
             # this should send to an address that isn't listening
             await self.loop.sock_sendto(socket_1, b'c', addr_3)
-            self.assertEqual(await socket_1_recv_task, b'')
+            with self.assertRaises(ConnectionResetError):
+                await socket_1_recv_task
             socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
             await asyncio.sleep(0)
 
@@ -706,6 +699,22 @@ async def recvfrom_into(socket):
                 self._basetest_datagram_send_to_non_listening_address(
                     recvfrom_into))
 
+
+    class SelectEventLoopTests(_DatagramSendToNonListeningAddressMixin,
+                               BaseSockTestsMixin,
+                               test_utils.TestCase):
+
+        def create_event_loop(self):
+            return asyncio.SelectorEventLoop()
+
+
+    class ProactorEventLoopTests(_DatagramSendToNonListeningAddressMixin,
+                                 BaseSockTestsMixin,
+                                 test_utils.TestCase):
+
+        def create_event_loop(self):
+            return asyncio.ProactorEventLoop()
+
 else:
     import selectors
 
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-01-00-00-00.gh-issue-156784.8gvtt4.rst 
b/Misc/NEWS.d/next/Library/2026-09-01-00-00-00.gh-issue-156784.8gvtt4.rst
new file mode 100644
index 00000000000000..699448059114ec
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-01-00-00-00.gh-issue-156784.8gvtt4.rst
@@ -0,0 +1,3 @@
+:class:`asyncio.ProactorEventLoop` no longer silently ignores
+:exc:`ConnectionResetError` on UDP sockets, matching
+:class:`asyncio.SelectorEventLoop`.

_______________________________________________
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