diff --git a/src/easynetwork_asyncio/stream/listener.py b/src/easynetwork_asyncio/stream/listener.py index 526211eb..988f1465 100644 --- a/src/easynetwork_asyncio/stream/listener.py +++ b/src/easynetwork_asyncio/stream/listener.py @@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, Any, Generic, NoReturn, TypeVar, final from easynetwork.lowlevel.api_async.transports import abc as transports -from easynetwork.lowlevel.constants import ACCEPT_CAPACITY_ERRNOS, ACCEPT_CAPACITY_ERROR_SLEEP_TIME +from easynetwork.lowlevel.constants import ACCEPT_CAPACITY_ERRNOS, ACCEPT_CAPACITY_ERROR_SLEEP_TIME, NOT_CONNECTED_SOCKET_ERRNOS from easynetwork.lowlevel.socket import _get_socket_extra from ..socket import AsyncSocket @@ -107,7 +107,13 @@ async def client_task(client_socket: _socket.socket) -> None: except BaseException as exc: client_socket.close() - self.__accepted_socket_factory.log_connection_error(logger, exc) + if isinstance(exc, OSError) and exc.errno in NOT_CONNECTED_SOCKET_ERRNOS: + # The remote host closed the connection before starting the task. + # See this test for details: + # test____serve_forever____accept_client____client_sent_RST_packet_right_after_accept + logger.warning("A client connection was interrupted just after listener.accept()") + else: + self.__accepted_socket_factory.log_connection_error(logger, exc) # Only reraise base exceptions if not isinstance(exc, Exception): diff --git a/tests/unit_test/test_async/test_asyncio_backend/test_stream.py b/tests/unit_test/test_async/test_asyncio_backend/test_stream.py index a0a63c1f..47d768f7 100644 --- a/tests/unit_test/test_async/test_asyncio_backend/test_stream.py +++ b/tests/unit_test/test_async/test_asyncio_backend/test_stream.py @@ -13,7 +13,7 @@ from socket import SHUT_RDWR, SHUT_WR from typing import TYPE_CHECKING, Any -from easynetwork.lowlevel.constants import ACCEPT_CAPACITY_ERRNOS +from easynetwork.lowlevel.constants import ACCEPT_CAPACITY_ERRNOS, NOT_CONNECTED_SOCKET_ERRNOS from easynetwork.lowlevel.socket import SocketAttribute, TLSAttribute from easynetwork_asyncio.stream.listener import ( AbstractAcceptedSocketFactory, @@ -426,10 +426,19 @@ async def test____serve____default( accepted_socket_factory.connect.assert_awaited_once_with(client_socket, event_loop) handler.assert_awaited_once_with(stream) - @pytest.mark.parametrize("exception_cls", [Exception, asyncio.CancelledError, BaseException]) + @pytest.mark.parametrize( + "exc", + [ + *(OSError(errno, os.strerror(errno)) for errno in sorted(NOT_CONNECTED_SOCKET_ERRNOS)), + Exception(), + asyncio.CancelledError(), + BaseException(), + ], + ids=repr, + ) async def test____serve____connect____error_raised( self, - exception_cls: type[BaseException], + exc: BaseException, event_loop: asyncio.AbstractEventLoop, listener: ListenerSocketAdapter[Any], mock_async_socket: MagicMock, @@ -437,16 +446,17 @@ async def test____serve____connect____error_raised( handler: AsyncMock, mock_tcp_socket_factory: Callable[[], MagicMock], fake_cancellation_cls: type[BaseException], + caplog: pytest.LogCaptureFixture, mocker: MockerFixture, ) -> None: # Arrange + caplog.set_level(logging.DEBUG) client_socket = mock_tcp_socket_factory() - exc = exception_cls() accepted_socket_factory.connect.side_effect = exc mock_async_socket.accept.side_effect = [client_socket, fake_cancellation_cls] # Act - with pytest.raises(BaseExceptionGroup) if exception_cls is BaseException else contextlib.nullcontext(): + with pytest.raises(BaseExceptionGroup) if type(exc) is BaseException else contextlib.nullcontext(): async with AsyncIOTaskGroup() as task_group: with pytest.raises(fake_cancellation_cls): await listener.serve(handler, task_group) @@ -458,8 +468,15 @@ async def test____serve____connect____error_raised( match exc: case asyncio.CancelledError(): + assert len(caplog.records) == 0 accepted_socket_factory.log_connection_error.assert_not_called() + case OSError(): + # ENOTCONN error should not create a big Traceback error but only a warning (at least) + assert len(caplog.records) == 1 + assert caplog.records[0].levelno == logging.WARNING + assert caplog.records[0].message == "A client connection was interrupted just after listener.accept()" case _: + assert len(caplog.records) == 0 accepted_socket_factory.log_connection_error.assert_called_once_with( mocker.ANY, # logger exc,