Send localization info on websocket_api script errors (#104638)

* Send localization info on script errors

* Use connection exception hander

* Keep HomeAssistantError is unknown_error

* Move specific exception handling
This commit is contained in:
Jan Bouwhuis 2023-11-29 10:47:23 +01:00 committed by GitHub
parent 7dbaf40f48
commit efd330f182
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 3 deletions

View file

@ -17,6 +17,7 @@ from .const import ( # noqa: F401
ERR_INVALID_FORMAT,
ERR_NOT_FOUND,
ERR_NOT_SUPPORTED,
ERR_SERVICE_VALIDATION_ERROR,
ERR_TEMPLATE_ERROR,
ERR_TIMEOUT,
ERR_UNAUTHORIZED,

View file

@ -778,7 +778,25 @@ async def handle_execute_script(
context = connection.context(msg)
script_obj = Script(hass, script_config, f"{const.DOMAIN} script", const.DOMAIN)
script_result = await script_obj.async_run(msg.get("variables"), context=context)
try:
script_result = await script_obj.async_run(
msg.get("variables"), context=context
)
except ServiceValidationError as err:
connection.logger.error(err)
connection.logger.debug("", exc_info=err)
connection.send_error(
msg["id"],
const.ERR_SERVICE_VALIDATION_ERROR,
str(err),
translation_domain=err.translation_domain,
translation_key=err.translation_key,
translation_placeholders=err.translation_placeholders,
)
return
except Exception as exc: # pylint: disable=broad-except
connection.async_handle_exception(msg, exc)
return
connection.send_result(
msg["id"],
{

View file

@ -255,7 +255,10 @@ class ActiveConnection:
log_handler = self.logger.error
code = const.ERR_UNKNOWN_ERROR
err_message = None
err_message: str | None = None
translation_domain: str | None = None
translation_key: str | None = None
translation_placeholders: dict[str, Any] | None = None
if isinstance(err, Unauthorized):
code = const.ERR_UNAUTHORIZED
@ -268,6 +271,10 @@ class ActiveConnection:
err_message = "Timeout"
elif isinstance(err, HomeAssistantError):
err_message = str(err)
code = const.ERR_UNKNOWN_ERROR
translation_domain = err.translation_domain
translation_key = err.translation_key
translation_placeholders = err.translation_placeholders
# This if-check matches all other errors but also matches errors which
# result in an empty message. In that case we will also log the stack
@ -276,7 +283,16 @@ class ActiveConnection:
err_message = "Unknown error"
log_handler = self.logger.exception
self.send_message(messages.error_message(msg["id"], code, err_message))
self.send_message(
messages.error_message(
msg["id"],
code,
err_message,
translation_domain=translation_domain,
translation_key=translation_key,
translation_placeholders=translation_placeholders,
)
)
if code:
err_message += f" ({code})"

View file

@ -297,6 +297,7 @@ def async_mock_service(
schema: vol.Schema | None = None,
response: ServiceResponse = None,
supports_response: SupportsResponse | None = None,
raise_exception: Exception | None = None,
) -> list[ServiceCall]:
"""Set up a fake service & return a calls log list to this service."""
calls = []
@ -305,6 +306,8 @@ def async_mock_service(
def mock_service_log(call): # pylint: disable=unnecessary-lambda
"""Mock service call."""
calls.append(call)
if raise_exception is not None:
raise raise_exception
return response
if supports_response is None:

View file

@ -2317,6 +2317,65 @@ async def test_execute_script(
assert call.context.as_dict() == msg_var["result"]["context"]
@pytest.mark.parametrize(
("raise_exception", "err_code"),
[
(
HomeAssistantError(
"Some error",
translation_domain="test",
translation_key="test_error",
translation_placeholders={"option": "bla"},
),
"unknown_error",
),
(
ServiceValidationError(
"Some error",
translation_domain="test",
translation_key="test_error",
translation_placeholders={"option": "bla"},
),
"service_validation_error",
),
],
)
async def test_execute_script_err_localization(
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
raise_exception: HomeAssistantError,
err_code: str,
) -> None:
"""Test testing a condition."""
async_mock_service(
hass, "domain_test", "test_service", raise_exception=raise_exception
)
await websocket_client.send_json(
{
"id": 5,
"type": "execute_script",
"sequence": [
{
"service": "domain_test.test_service",
"data": {"hello": "world"},
},
{"stop": "done", "response_variable": "service_result"},
],
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"] is False
assert msg["error"]["code"] == err_code
assert msg["error"]["message"] == "Some error"
assert msg["error"]["translation_key"] == "test_error"
assert msg["error"]["translation_domain"] == "test"
assert msg["error"]["translation_placeholders"] == {"option": "bla"}
async def test_execute_script_complex_response(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:

View file

@ -43,6 +43,12 @@ from tests.common import MockUser
"Failed to do X",
"Error handling message: Failed to do X (unknown_error) Mock User from 127.0.0.42 (Browser)",
),
(
exceptions.ServiceValidationError("Failed to do X"),
websocket_api.ERR_UNKNOWN_ERROR,
"Failed to do X",
"Error handling message: Failed to do X (unknown_error) Mock User from 127.0.0.42 (Browser)",
),
(
ValueError("Really bad"),
websocket_api.ERR_UNKNOWN_ERROR,