Fix API POST endpoints json parsing error-handling (#134326)

* Fix API POST endpoints json parsing error-handling

* Add tests

* Fix mypy and ruff errors

* Fix coverage by removing non-needed error handling

* Correct error handling and improve tests

---------

Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
Nathan Larsen 2025-06-25 09:58:21 -05:00 committed by GitHub
parent 809aced9cc
commit e210681751
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 67 additions and 3 deletions

View File

@ -260,11 +260,18 @@ class APIEntityStateView(HomeAssistantView):
if not user.is_admin:
raise Unauthorized(entity_id=entity_id)
hass = request.app[KEY_HASS]
body = await request.text()
try:
data = await request.json()
data: Any = json_loads(body) if body else None
except ValueError:
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
if not isinstance(data, dict):
return self.json_message(
"State data should be a JSON object.", HTTPStatus.BAD_REQUEST
)
if (new_state := data.get("state")) is None:
return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST)
@ -477,9 +484,19 @@ class APITemplateView(HomeAssistantView):
@require_admin
async def post(self, request: web.Request) -> web.Response:
"""Render a template."""
body = await request.text()
try:
data: Any = json_loads(body) if body else None
except ValueError:
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
if not isinstance(data, dict):
return self.json_message(
"Template data should be a JSON object.", HTTPStatus.BAD_REQUEST
)
tpl = _cached_template(data["template"], request.app[KEY_HASS])
try:
data = await request.json()
tpl = _cached_template(data["template"], request.app[KEY_HASS])
return tpl.async_render(variables=data.get("variables"), parse_result=False) # type: ignore[no-any-return]
except (ValueError, TemplateError) as ex:
return self.json_message(

View File

@ -129,6 +129,28 @@ async def test_api_state_change_with_bad_data(
assert resp.status == HTTPStatus.BAD_REQUEST
async def test_api_state_change_with_invalid_json(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test if API sends appropriate error if send invalid json data."""
resp = await mock_api_client.post("/api/states/test.test", data="{,}")
assert resp.status == HTTPStatus.BAD_REQUEST
assert await resp.json() == {"message": "Invalid JSON specified."}
async def test_api_state_change_with_string_body(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test if API sends appropriate error if we send a string instead of a JSON object."""
resp = await mock_api_client.post(
"/api/states/bad.entity.id", json='"{"state": "new_state"}"'
)
assert resp.status == HTTPStatus.BAD_REQUEST
assert await resp.json() == {"message": "State data should be a JSON object."}
async def test_api_state_change_to_zero_value(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
@ -529,6 +551,31 @@ async def test_api_template_error(
assert resp.status == HTTPStatus.BAD_REQUEST
async def test_api_template_with_invalid_json(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test if API sends appropriate error if send invalid json data."""
resp = await mock_api_client.post(const.URL_API_TEMPLATE, data="{,}")
assert resp.status == HTTPStatus.BAD_REQUEST
assert await resp.json() == {"message": "Invalid JSON specified."}
async def test_api_template_error_with_string_body(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test that the API returns an appropriate error when a string is sent in the body."""
hass.states.async_set("sensor.temperature", 10)
resp = await mock_api_client.post(
const.URL_API_TEMPLATE,
json='"{"template": "{{ states.sensor.temperature.state"}"',
)
assert resp.status == HTTPStatus.BAD_REQUEST
assert await resp.json() == {"message": "Template data should be a JSON object."}
async def test_stream(hass: HomeAssistant, mock_api_client: TestClient) -> None:
"""Test the stream."""
listen_count = _listen_count(hass)