Compare commits

...

2 Commits

Author SHA1 Message Date
Stefan Agner
3e307c5c8b Fix pylint 2025-10-08 14:56:56 +02:00
Stefan Agner
1cd499b4a5 Fix WebSocket transport None race condition in proxy
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 <noreply@anthropic.com>
2025-10-08 14:48:08 +02:00
2 changed files with 32 additions and 1 deletions

View File

@@ -222,6 +222,11 @@ class APIProxy(CoreSysAttributes):
raise HTTPBadGateway() raise HTTPBadGateway()
_LOGGER.info("Home Assistant WebSocket API request initialize") _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 # init server
server = web.WebSocketResponse(heartbeat=30) server = web.WebSocketResponse(heartbeat=30)
await server.prepare(request) await server.prepare(request)

View File

@@ -9,7 +9,7 @@ import logging
from typing import Any, cast from typing import Any, cast
from unittest.mock import AsyncMock, patch 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.http_websocket import WSMessage, WSMsgType
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
import pytest import pytest
@@ -223,6 +223,32 @@ 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."""
# 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"]) @pytest.mark.parametrize("path", ["", "mock_path"])
async def test_api_proxy_get_request( async def test_api_proxy_get_request(
api_client: TestClient, api_client: TestClient,