From 3fc68b00a977c4a646ae8f73b90b4bf22c7fb863 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 10:21:31 -0500 Subject: [PATCH] Reduce log spam from unauthenticated websocket connections --- .../components/websocket_api/http.py | 15 +++++- tests/components/websocket_api/test_http.py | 49 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 4250da149ad..0e9e0eb6933 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -37,6 +37,7 @@ from .messages import message_to_json_bytes from .util import describe_request CLOSE_MSG_TYPES = {WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING} +AUTH_MESSAGE_TIMEOUT = 10 # seconds if TYPE_CHECKING: from .connection import ActiveConnection @@ -389,9 +390,11 @@ class WebSocketHandler: # Auth Phase try: - msg = await self._wsock.receive(10) + msg = await self._wsock.receive(AUTH_MESSAGE_TIMEOUT) except TimeoutError as err: - raise Disconnect("Did not receive auth message within 10 seconds") from err + raise Disconnect( + f"Did not receive auth message within {AUTH_MESSAGE_TIMEOUT} seconds" + ) from err if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): raise Disconnect("Received close message during auth phase") @@ -538,6 +541,14 @@ class WebSocketHandler: finally: if disconnect_warn is None: logger.debug("%s: Disconnected", self.description) + elif connection is None: + # Auth phase disconnects (connection is None) should be logged at debug level + # as they can be from random port scanners or non-legitimate connections + logger.debug( + "%s: Disconnected during auth phase: %s", + self.description, + disconnect_warn, + ) else: logger.warning( "%s: Disconnected: %s", self.description, disconnect_warn diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index b4b11d9cf02..2e60e837976 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta +import logging from typing import Any, cast from unittest.mock import patch @@ -20,7 +21,11 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_call_logger_set_level, async_fire_time_changed -from tests.typing import MockHAClientWebSocket, WebSocketGenerator +from tests.typing import ( + ClientSessionGenerator, + MockHAClientWebSocket, + WebSocketGenerator, +) @pytest.fixture @@ -400,6 +405,48 @@ async def test_prepare_fail_connection_reset( assert "Connection reset by peer while preparing WebSocket" in caplog.text +async def test_auth_timeout_logs_at_debug( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test auth timeout is logged at debug level not warning.""" + # Setup websocket API + assert await async_setup_component(hass, "websocket_api", {}) + + client = await hass_client() + + # Patch the auth timeout to be very short (0.001 seconds) + with ( + caplog.at_level(logging.DEBUG, "homeassistant.components.websocket_api"), + patch( + "homeassistant.components.websocket_api.http.AUTH_MESSAGE_TIMEOUT", 0.001 + ), + ): + # Try to connect - will timeout quickly since we don't send auth + ws = await client.ws_connect("/api/websocket") + # Wait a bit for the timeout to trigger and cleanup to complete + await asyncio.sleep(0.1) + await ws.close() + await asyncio.sleep(0.1) + + # Check that "Did not receive auth message" is logged at debug, not warning + debug_messages = [ + r.message for r in caplog.records if r.levelno == logging.DEBUG + ] + assert any( + "Disconnected during auth phase: Did not receive auth message" in msg + for msg in debug_messages + ) + + # Check it's NOT logged at warning level + warning_messages = [ + r.message for r in caplog.records if r.levelno >= logging.WARNING + ] + for msg in warning_messages: + assert "Did not receive auth message" not in msg + + async def test_enable_coalesce( hass: HomeAssistant, hass_ws_client: WebSocketGenerator,