From b897ca726025713c55c621231966a34ca698b19d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Oct 2020 09:38:24 -0500 Subject: [PATCH] Ensure all template errors are caught and the websocket api reports them (#41719) --- .../components/websocket_api/commands.py | 21 +++++++---- homeassistant/exceptions.py | 4 +-- homeassistant/helpers/template.py | 2 +- .../components/websocket_api/test_commands.py | 35 +++++++++++++++++++ tests/helpers/test_template.py | 10 ++++++ 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 11d97f58f50..2106e745351 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -257,13 +257,20 @@ async def handle_render_template(hass, connection, msg): timeout = msg.get("timeout") info = None - if timeout and await template.async_render_will_timeout(timeout): - connection.send_error( - msg["id"], - const.ERR_TEMPLATE_ERROR, - f"Exceeded maximum execution time of {timeout}s", - ) - return + if timeout: + try: + timed_out = await template.async_render_will_timeout(timeout) + except TemplateError as ex: + connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) + return + + if timed_out: + connection.send_error( + msg["id"], + const.ERR_TEMPLATE_ERROR, + f"Exceeded maximum execution time of {timeout}s", + ) + return @callback def _template_listener(event, updates): diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 44587fec043..015e81f2ca0 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,8 +1,6 @@ """The exceptions used by Home Assistant.""" from typing import TYPE_CHECKING, Optional -import jinja2 - if TYPE_CHECKING: from .core import Context # noqa: F401 pylint: disable=unused-import @@ -22,7 +20,7 @@ class NoEntitySpecifiedError(HomeAssistantError): class TemplateError(HomeAssistantError): """Error during template rendering.""" - def __init__(self, exception: jinja2.TemplateError) -> None: + def __init__(self, exception: Exception) -> None: """Init the error.""" super().__init__(f"{exception.__class__.__name__}: {exception}") diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 0f3d605523c..b92f926f809 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -331,7 +331,7 @@ class Template: try: render_result = compiled.render(kwargs) - except jinja2.TemplateError as err: + except Exception as err: # pylint: disable=broad-except raise TemplateError(err) from err render_result = render_result.strip() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index e05108c46bc..b166b2bb95e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -485,6 +485,41 @@ async def test_render_template_with_error(hass, websocket_client, caplog): assert "TemplateError" not in caplog.text +async def test_render_template_with_timeout_and_error(hass, websocket_client, caplog): + """Test a template with an error with a timeout.""" + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "{{ now() | rando }}", + "timeout": 5, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text + + +async def test_render_template_error_in_template_code(hass, websocket_client, caplog): + """Test a template that will throw in template.py.""" + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": "{{ now() | random }}"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text + + async def test_render_template_with_delayed_error(hass, websocket_client, caplog): """Test a template with an error that only happens after a state change.""" hass.states.async_set("sensor.test", "on") diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 37c9255f1ae..3b1e7c94a29 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2533,6 +2533,16 @@ async def test_lights(hass): assert f"sensor{i}" in info.result() +async def test_template_errors(hass): + """Test template rendering wraps exceptions with TemplateError.""" + + with pytest.raises(TemplateError): + template.Template("{{ now() | rando }}", hass).async_render() + + with pytest.raises(TemplateError): + template.Template("{{ now() | random }}", hass).async_render() + + async def test_state_attributes(hass): """Test state attributes.""" hass.states.async_set("sensor.test", "23")