diff --git a/test/test_api_v3_on_demand_socket.py b/test/test_api_v3_on_demand_socket.py index d6769fb1..8498b8bd 100644 --- a/test/test_api_v3_on_demand_socket.py +++ b/test/test_api_v3_on_demand_socket.py @@ -127,6 +127,20 @@ class TestMailboxFallback: assert resp.get_json()["data"]["socket_error"] == "internal" assert len(_mailbox_writes(service["cache"])) == 1 + def test_an_unknown_reason_is_reported_as_other(self, api_v3_client, service): + # Only known codes are echoed back; anything else stays server-side. + with patch(f"{CLIENT}.on_demand_start", + side_effect=control_client.ControlError("/run/secret/path", "x")): + data = api_v3_client.post(START_URL, json={"plugin_id": "weather"}).get_json()["data"] + assert data["transport"] == "mailbox" + assert data["socket_error"] == "other" + assert len(_mailbox_writes(service["cache"])) == 1 + + def test_every_display_error_code_is_reportable(self): + from web_interface.blueprints.api_v3 import display + codes = {v for k, v in vars(c.ErrorCode).items() if not k.startswith("_")} + assert codes <= set(display._REPORTABLE_SOCKET_REASONS) + def test_stop_falls_back(self, api_v3_client, service): with patch(f"{CLIENT}.on_demand_stop", side_effect=control_client.ControlError("timeout")): diff --git a/web_interface/blueprints/api_v3/display.py b/web_interface/blueprints/api_v3/display.py index 3b38c955..a2fc74ba 100644 --- a/web_interface/blueprints/api_v3/display.py +++ b/web_interface/blueprints/api_v3/display.py @@ -34,6 +34,21 @@ def _cache_manager(): #: older than the socket, Windows, or switched off. Not worth a log line. _QUIET_SOCKET_REASONS = frozenset({'no_socket', 'disabled', 'unsupported'}) +#: Every reason code a response may echo as ``socket_error``: the client's +#: transport reasons plus the display's ErrorCode values. Anything else is +#: reported as ``other``, so no text taken from an exception reaches a reply. +_REPORTABLE_SOCKET_REASONS = ( + 'disabled', 'unsupported', 'no_socket', 'refused', 'timeout', 'closed', + 'bad_response', 'invalid_request', + 'bad_json', 'bad_request', 'message_too_large', 'unsupported_version', + 'unknown_command', 'invalid_args', 'busy', 'forbidden', 'internal', +) + + +def _socket_reason_code(reason): + """``reason`` as one of _REPORTABLE_SOCKET_REASONS, else ``'other'``.""" + return next((code for code in _REPORTABLE_SOCKET_REASONS if code == reason), 'other') + def _deliver_on_demand(payload): """Hand an on-demand request to the display: control socket, else mailbox. @@ -58,7 +73,7 @@ def _deliver_on_demand(payload): control_client.on_demand_stop(payload['request_id']) return 'socket', None except control_client.ControlError as e: - reason = e.reason + reason = _socket_reason_code(e.reason) if reason in _QUIET_SOCKET_REASONS: logger.debug("On-demand %s via the mailbox: %s", payload['action'], e) else: