Fix Supervisor connection leak when leaving an app's log page (#183772)

This commit is contained in:
Jeffrey Larson
2026-10-05 11:00:30 +00:00
committed by Franck Nijhof
parent f12df14332
commit 0ab41b97f5
3 changed files with 46 additions and 18 deletions
+15 -16
View File
@@ -178,29 +178,28 @@ class HassIOView(HomeAssistantView):
headers[RANGE] = request.headers[RANGE]
try:
client = await self._websession.request(
async with self._websession.request(
method=request.method,
url=f"http://{self._host}/{quote(path)}",
params=request.query,
data=request.content if request.method != "GET" else None,
headers=headers,
timeout=_get_timeout(path),
)
) as client:
# Stream response
response = web.StreamResponse(
status=client.status, headers=_response_header(client)
)
response.content_type = client.content_type
# Stream response
response = web.StreamResponse(
status=client.status, headers=_response_header(client)
)
response.content_type = client.content_type
if should_compress(response.content_type, path):
response.enable_compression()
await response.prepare(request)
# In testing iter_chunked, iter_any, and iter_chunks:
# iter_chunks was the best performing option since
# it does not have to do as much re-assembly
async for data, _ in client.content.iter_chunks():
await response.write(data)
if should_compress(response.content_type, path):
response.enable_compression()
await response.prepare(request)
# In testing iter_chunked, iter_any, and iter_chunks:
# iter_chunks was the best performing option since
# it does not have to do as much re-assembly
async for data, _ in client.content.iter_chunks():
await response.write(data)
except aiohttp.ClientError as err:
_LOGGER.error("Client error on api %s request %s", path, err)
+30 -2
View File
@@ -1,13 +1,16 @@
"""The tests for the hassio component."""
import asyncio
from contextlib import suppress
from http import HTTPStatus
from unittest.mock import Mock, PropertyMock, patch
from aiohttp import StreamReader
from aiohttp import ClientError, StreamReader
from aiohttp.test_utils import TestClient
import pytest
from tests.common import MockUser
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
@pytest.fixture
@@ -478,3 +481,28 @@ async def test_forward_range_header_for_logs(
assert aioclient_mock.mock_calls[3][-1].get("Range") == test_range
assert aioclient_mock.mock_calls[4][-1].get("Range") == test_range
assert aioclient_mock.mock_calls[5][-1].get("Range") is None
async def test_supervisor_response_released_when_stream_cancelled(
hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the response from the Supervisor is released when the stream is cancelled."""
# A frontend disconnect cancels the handler, simulated here from the stream.
stream = StreamReader(Mock(), limit=2**16)
stream.feed_data(b"line 1\n")
stream.set_exception(asyncio.CancelledError())
aioclient_mock.get("http://127.0.0.1/addons/bl_b392/logs/follow")
with (
patch.object(
AiohttpClientMockResponse, "content", new_callable=PropertyMock
) as mock_content,
patch.object(AiohttpClientMockResponse, "release") as mock_release,
):
mock_content.return_value = stream
resp = await hassio_client.get("/api/hassio/addons/bl_b392/logs/follow")
with suppress(ClientError):
await resp.read()
mock_release.assert_called_once()
+1
View File
@@ -336,6 +336,7 @@ class AiohttpClientMockResponse:
exc_tb: TracebackType | None,
) -> None:
"""Exit the context manager."""
self.release()
@contextmanager