diff --git a/homeassistant/components/http/websocket_api.py b/homeassistant/components/http/websocket_api.py index c2f0e175fc75..c15bcb842f93 100644 --- a/homeassistant/components/http/websocket_api.py +++ b/homeassistant/components/http/websocket_api.py @@ -9,7 +9,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HASS_DOMAIN, SERVICE_HOMEASSISTANT_RESTART, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from .config import HTTP_STORAGE_SCHEMA, ConfData, async_get_and_load_store @@ -17,6 +17,7 @@ from .const import ATTR_CONFIG, CONF_SERVER_PORT from .server import async_verify_can_bind ERR_BIND_FAILED: Final = "bind_failed" +ERR_NOT_RUNNING: Final = "not_running" @callback @@ -74,6 +75,10 @@ async def websocket_set_config( ) -> None: """Store a new pending HTTP configuration and restart to apply it. + Only allowed while Home Assistant is running: applying a config means + restarting, and restarting a start that has not finished yet leaves + integrations that are still setting up in an undefined state. + A new config is first verified to be applicable by binding its configured address, so an unusable config is rejected here instead of being discovered after the restart. The check is skipped when the port @@ -85,6 +90,15 @@ async def websocket_set_config( refreshed. The result reports whether a restart was triggered via ``{"restart": bool}``. """ + if hass.state is not CoreState.running: + connection.send_error( + msg["id"], + ERR_NOT_RUNNING, + "The HTTP configuration can only be changed while Home Assistant " + f"is running, current state: {hass.state.value}", + ) + return + config: ConfData | None = msg[ATTR_CONFIG] if config is not None and config[CONF_SERVER_PORT] != hass.http.server_port: try: diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 11eb1839c25b..a9802f8469a9 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -33,7 +33,7 @@ from homeassistant.components.http.config import ( ) from homeassistant.components.http.const import ENV_SETUP_PORT, ENV_SUPERVISOR from homeassistant.const import EVENT_HOMEASSISTANT_STOP, HASSIO_USER_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.http import KEY_HASS @@ -2265,6 +2265,58 @@ async def test_websocket_configure_same_port_skips_bind_check( assert len(restart_calls) == 1 +@pytest.mark.parametrize( + "core_state", + [CoreState.not_running, CoreState.starting], + ids=["not-running", "starting"], +) +async def test_websocket_configure_rejected_while_not_running( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + mock_create_server: Mock, + core_state: CoreState, +) -> None: + """Staging a new config is rejected while Home Assistant is not running yet.""" + assert await async_setup_component(hass, DOMAIN, {}) + await async_setup_component(hass, "websocket_api", {}) + await hass.async_start() + await hass.async_block_till_done() + + restart_calls = async_mock_service(hass, "homeassistant", "restart") + ws_client = await hass_ws_client(hass) + + hass.set_state(core_state) + await ws_client.send_json_auto_id( + {"type": "http/config/configure", "config": {"server_port": 9123}} + ) + response = await ws_client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "not_running" + assert response["error"]["message"] == ( + "The HTTP configuration can only be changed while Home Assistant is " + f"running, current state: {core_state.value}" + ) + + # The config is neither probed nor stored, and no restart is triggered. + assert mock_create_server.call_count == 1 + assert hass_storage[DOMAIN]["data"]["pending"] is None + assert len(restart_calls) == 0 + + # Once the start finished, the same config is accepted. + hass.set_state(CoreState.running) + await ws_client.send_json_auto_id( + {"type": "http/config/configure", "config": {"server_port": 9123}} + ) + response = await ws_client.receive_json() + assert response["success"] + assert response["result"] == {"restart": True} + assert hass_storage[DOMAIN]["data"]["pending"]["server_port"] == 9123 + + await hass.async_block_till_done() + assert len(restart_calls) == 1 + + async def test_pending_config_auto_reverts_to_stable( hass: HomeAssistant, hass_ws_client: WebSocketGenerator,