Skip to content

Commit 0815fe3

Browse files
authored
gh-156784: do not ignore error port unreachable in proactor event loop implementation (#156788)
1 parent 59942e2 commit 0815fe3

4 files changed

Lines changed: 57 additions & 34 deletions

File tree

‎Lib/asyncio/windows_events.py‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import _overlapped
99
import _winapi
1010
import errno
11-
from functools import partial
1211
import math
1312
import msvcrt
1413
import socket
@@ -460,24 +459,20 @@ def finish_socket_func(trans, key, ov):
460459
try:
461460
return ov.getresult()
462461
except OSError as exc:
462+
# ERROR_PORT_UNREACHABLE is reported by WSARecvFrom when the
463+
# same socket was previously used to send to an address that
464+
# isn't listening (gh-91227). Surface it as a
465+
# ConnectionResetError, like the other recoverable codes here,
466+
# so it propagates to the caller the same way a plain
467+
# socket.recvfrom() already does on SelectorEventLoop, instead
468+
# of being silently swallowed.
463469
if exc.winerror in (_overlapped.ERROR_NETNAME_DELETED,
464-
_overlapped.ERROR_OPERATION_ABORTED):
470+
_overlapped.ERROR_OPERATION_ABORTED,
471+
_overlapped.ERROR_PORT_UNREACHABLE):
465472
raise ConnectionResetError(*exc.args)
466473
else:
467474
raise
468475

469-
@classmethod
470-
def _finish_recvfrom(cls, trans, key, ov, *, empty_result):
471-
try:
472-
return cls.finish_socket_func(trans, key, ov)
473-
except OSError as exc:
474-
# WSARecvFrom will report ERROR_PORT_UNREACHABLE when the same
475-
# socket is used to send to an address that is not listening.
476-
if exc.winerror == _overlapped.ERROR_PORT_UNREACHABLE:
477-
return empty_result, None
478-
else:
479-
raise
480-
481476
def recv(self, conn, nbytes, flags=0):
482477
self._register_with_iocp(conn)
483478
ov = _overlapped.Overlapped(NULL)
@@ -512,8 +507,7 @@ def recvfrom(self, conn, nbytes, flags=0):
512507
except BrokenPipeError:
513508
return self._result((b'', None))
514509

515-
return self._register(ov, conn, partial(self._finish_recvfrom,
516-
empty_result=b''))
510+
return self._register(ov, conn, self.finish_socket_func)
517511

518512
def recvfrom_into(self, conn, buf, nbytes=0):
519513
self._register_with_iocp(conn)
@@ -523,8 +517,7 @@ def recvfrom_into(self, conn, buf, nbytes=0):
523517
except BrokenPipeError:
524518
return self._result((0, None))
525519

526-
return self._register(ov, conn, partial(self._finish_recvfrom,
527-
empty_result=0))
520+
return self._register(ov, conn, self.finish_socket_func)
528521

529522
def sendto(self, conn, buf, flags=0, addr=None):
530523
self._register_with_iocp(conn)

‎Lib/test/test_asyncio/test_events.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,9 +1523,18 @@ class Protocol(asyncio.DatagramProtocol):
15231523

15241524
_received_datagram = None
15251525

1526+
def connection_made(self, transport):
1527+
self.errors = []
1528+
self.error_received_event = loop.create_future()
1529+
15261530
def datagram_received(self, data, addr):
15271531
self._received_datagram.set_result(data)
15281532

1533+
def error_received(self, exc):
1534+
self.errors.append(exc)
1535+
if not self.error_received_event.done():
1536+
self.error_received_event.set_result(None)
1537+
15291538
async def wait_for_datagram_received(self):
15301539
self._received_datagram = loop.create_future()
15311540
result = await asyncio.wait_for(self._received_datagram, 10)
@@ -1580,6 +1589,15 @@ def create_socket():
15801589
protocol_1.wait_for_datagram_received()
15811590
), b'd')
15821591

1592+
if sys.platform == 'win32':
1593+
# The bad send to addr_3 should be surfaced to the protocol
1594+
# via error_received() instead of being silently dropped,
1595+
# while transport_1 keeps working as shown above.
1596+
loop.run_until_complete(
1597+
asyncio.wait_for(protocol_1.error_received_event, 10))
1598+
self.assertTrue(protocol_1.errors)
1599+
self.assertIsInstance(protocol_1.errors[0], ConnectionResetError)
1600+
15831601
transport_1.close()
15841602
transport_2.close()
15851603

‎Lib/test/test_asyncio/test_sock_lowlevel.py‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -613,19 +613,10 @@ def test_create_connection_sock(self):
613613

614614
if sys.platform == 'win32':
615615

616-
class SelectEventLoopTests(BaseSockTestsMixin,
617-
test_utils.TestCase):
618-
619-
def create_event_loop(self):
620-
return asyncio.SelectorEventLoop()
621-
622-
623-
class ProactorEventLoopTests(BaseSockTestsMixin,
624-
test_utils.TestCase):
625-
626-
def create_event_loop(self):
627-
return asyncio.ProactorEventLoop()
628-
616+
class _DatagramSendToNonListeningAddressMixin:
617+
# Shared by SelectEventLoopTests and ProactorEventLoopTests so that
618+
# sock_recvfrom()/sock_recvfrom_into() behave identically on both
619+
# event loop implementations.
629620

630621
async def _basetest_datagram_send_to_non_listening_address(self,
631622
recvfrom):
@@ -634,8 +625,9 @@ async def _basetest_datagram_send_to_non_listening_address(self,
634625
# https://github.com/python/cpython/issues/88906
635626
# https://bugs.python.org/issue47071
636627
# https://bugs.python.org/issue44743
637-
# The Proactor event loop would fail to receive datagram messages
638-
# after sending a message to an address that wasn't listening.
628+
# Sending a datagram to an address that isn't listening can
629+
# surface as a ConnectionResetError on a later receive; the
630+
# socket must still be usable afterwards.
639631

640632
def create_socket():
641633
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -670,7 +662,8 @@ def create_socket():
670662

671663
# this should send to an address that isn't listening
672664
await self.loop.sock_sendto(socket_1, b'c', addr_3)
673-
self.assertEqual(await socket_1_recv_task, b'')
665+
with self.assertRaises(ConnectionResetError):
666+
await socket_1_recv_task
674667
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
675668
await asyncio.sleep(0)
676669

@@ -706,6 +699,22 @@ async def recvfrom_into(socket):
706699
self._basetest_datagram_send_to_non_listening_address(
707700
recvfrom_into))
708701

702+
703+
class SelectEventLoopTests(_DatagramSendToNonListeningAddressMixin,
704+
BaseSockTestsMixin,
705+
test_utils.TestCase):
706+
707+
def create_event_loop(self):
708+
return asyncio.SelectorEventLoop()
709+
710+
711+
class ProactorEventLoopTests(_DatagramSendToNonListeningAddressMixin,
712+
BaseSockTestsMixin,
713+
test_utils.TestCase):
714+
715+
def create_event_loop(self):
716+
return asyncio.ProactorEventLoop()
717+
709718
else:
710719
import selectors
711720

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`asyncio.ProactorEventLoop` no longer silently ignores
2+
:exc:`ConnectionResetError` on UDP sockets, matching
3+
:class:`asyncio.SelectorEventLoop`.

0 commit comments

Comments
 (0)