Compare commits

...

1 Commits

Author SHA1 Message Date
Stefan Agner
ff90e4b817 Fix UnboundLocalError when Core API fails post-update (#6761)
When get_config() raised HomeAssistantError after a Core update, the
except block set error_state and fell through to the frontend check,
which referenced an unbound `data` variable and raised UnboundLocalError.
That aborted the update with a JobException and skipped the rollback
path entirely.

Move the frontend checks into an else branch of the try/except so they
only run when get_config() succeeds. When it fails, error_state is set
and control falls through to the rollback logic below, which is what
PR #6726 intended.

Fixes SUPERVISOR-1JVX

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:05:40 +02:00
2 changed files with 61 additions and 15 deletions

View File

@@ -332,22 +332,22 @@ class HomeAssistantCore(JobGroup):
except HomeAssistantError:
# The API stopped responding between the update and now
self._error_state = True
# Verify that the frontend is loaded
if "frontend" not in data.get("components", []):
_LOGGER.error("API responds but frontend is not loaded")
self._error_state = True
# Check that the frontend is actually accessible
elif not await self.sys_homeassistant.api.check_frontend_available():
_LOGGER.error(
"Frontend component loaded but frontend is not accessible"
)
self._error_state = True
else:
# Health checks passed, clean up old image
with suppress(DockerError):
await self.instance.cleanup(old_image=old_image)
return
# Verify that the frontend is loaded
if "frontend" not in data.get("components", []):
_LOGGER.error("API responds but frontend is not loaded")
self._error_state = True
# Check that the frontend is actually accessible
elif not await self.sys_homeassistant.api.check_frontend_available():
_LOGGER.error(
"Frontend component loaded but frontend is not accessible"
)
self._error_state = True
else:
# Health checks passed, clean up old image
with suppress(DockerError):
await self.instance.cleanup(old_image=old_image)
return
# Update going wrong, revert it
if self.error_state and rollback:

View File

@@ -14,6 +14,7 @@ from supervisor.const import DNS_SUFFIX, CoreState
from supervisor.coresys import CoreSys
from supervisor.docker.homeassistant import DockerHomeAssistant
from supervisor.docker.interface import DockerInterface
from supervisor.exceptions import HomeAssistantError
from supervisor.homeassistant.api import APIState, HomeAssistantAPI
from supervisor.homeassistant.const import WSEvent
from supervisor.homeassistant.core import HomeAssistantCore
@@ -516,3 +517,48 @@ async def test_update_frontend_check_fails_triggers_rollback(
)
# Old image should not be cleaned up so rollback doesn't need to re-download
mock_cleanup.assert_not_called()
async def test_update_get_config_error_triggers_rollback(
api_client: TestClient,
coresys: CoreSys,
caplog: pytest.LogCaptureFixture,
tmp_supervisor_data: Path,
):
"""Test that update triggers rollback when get_config raises HomeAssistantError."""
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
coresys.homeassistant.version = AwesomeVersion("2025.8.0")
update_call_count = 0
async def mock_update(*args, **kwargs):
nonlocal update_call_count
update_call_count += 1
if update_call_count == 1:
coresys.homeassistant.version = AwesomeVersion("2025.8.3")
elif update_call_count == 2:
coresys.homeassistant.version = AwesomeVersion("2025.8.0")
with (
patch.object(DockerInterface, "update", new=mock_update),
patch.object(
DockerHomeAssistant,
"version",
new=PropertyMock(return_value=AwesomeVersion("2025.8.0")),
),
patch.object(HomeAssistantAPI, "get_config", side_effect=HomeAssistantError),
patch.object(
HomeAssistantAPI, "check_frontend_available", return_value=True
) as mock_check_frontend,
patch.object(DockerInterface, "cleanup") as mock_cleanup,
):
resp = await api_client.post("/core/update", json={"version": "2025.8.3"})
assert resp.status == 200
assert "HomeAssistant update failed -> rollback!" in caplog.text
assert update_call_count == 2
mock_check_frontend.assert_not_called()
assert (
Issue(IssueType.UPDATE_ROLLBACK, ContextType.CORE) in coresys.resolution.issues
)
mock_cleanup.assert_not_called()