From 1cd499b4a57cc04660ff270b0f69366b90d4ea81 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 8 Oct 2025 13:42:46 +0200 Subject: [PATCH 1/2] Fix WebSocket transport None race condition in proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a transport validity check before WebSocket upgrade to prevent AssertionError when clients disconnect during handshake. The issue occurs when a client connection is lost between the API state check and server.prepare() call, causing request.transport to become None and triggering "assert transport is not None" in aiohttp's _pre_start(). The fix detects the closed connection early and raises HTTPBadRequest with a clear reason instead of crashing with an AssertionError. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- supervisor/api/proxy.py | 5 +++++ tests/api/test_proxy.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/supervisor/api/proxy.py b/supervisor/api/proxy.py index 01078625dab..2d7ff49c8e4 100644 --- a/supervisor/api/proxy.py +++ b/supervisor/api/proxy.py @@ -222,6 +222,11 @@ async def websocket(self, request: web.Request): raise HTTPBadGateway() _LOGGER.info("Home Assistant WebSocket API request initialize") + # Check if transport is still valid before WebSocket upgrade + if request.transport is None: + _LOGGER.warning("WebSocket connection lost before upgrade") + raise web.HTTPBadRequest(reason="Connection closed") + # init server server = web.WebSocketResponse(heartbeat=30) await server.prepare(request) diff --git a/tests/api/test_proxy.py b/tests/api/test_proxy.py index 9694e4f3fa2..c598d50cf63 100644 --- a/tests/api/test_proxy.py +++ b/tests/api/test_proxy.py @@ -223,6 +223,34 @@ async def test_proxy_auth_abort_log( ) +async def test_websocket_transport_none( + coresys, + caplog: pytest.LogCaptureFixture, +): + """Test WebSocket connection with transport None is handled gracefully.""" + from aiohttp import web + + # Get the API proxy instance from coresys + api_proxy = APIProxy.__new__(APIProxy) + api_proxy.coresys = coresys + + # Create a mock request with transport set to None to simulate connection loss + mock_request = AsyncMock(spec=web.Request) + mock_request.transport = None + + caplog.clear() + with caplog.at_level(logging.WARNING): + # This should raise HTTPBadRequest, not AssertionError + with pytest.raises(web.HTTPBadRequest) as exc_info: + await api_proxy.websocket(mock_request) + + # Verify the error reason + assert exc_info.value.reason == "Connection closed" + + # Verify the warning was logged + assert "WebSocket connection lost before upgrade" in caplog.text + + @pytest.mark.parametrize("path", ["", "mock_path"]) async def test_api_proxy_get_request( api_client: TestClient, From 3e307c5c8bb53107e56e3f1ce1e9a40844b67cbe Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 8 Oct 2025 14:56:56 +0200 Subject: [PATCH 2/2] Fix pylint --- tests/api/test_proxy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/api/test_proxy.py b/tests/api/test_proxy.py index c598d50cf63..d8170705c37 100644 --- a/tests/api/test_proxy.py +++ b/tests/api/test_proxy.py @@ -9,7 +9,7 @@ from typing import Any, cast from unittest.mock import AsyncMock, patch -from aiohttp import ClientWebSocketResponse, WSCloseCode +from aiohttp import ClientWebSocketResponse, WSCloseCode, web from aiohttp.http_websocket import WSMessage, WSMsgType from aiohttp.test_utils import TestClient import pytest @@ -228,8 +228,6 @@ async def test_websocket_transport_none( caplog: pytest.LogCaptureFixture, ): """Test WebSocket connection with transport None is handled gracefully.""" - from aiohttp import web - # Get the API proxy instance from coresys api_proxy = APIProxy.__new__(APIProxy) api_proxy.coresys = coresys