diff --git a/headroom/proxy/loop_callback_failure_policy.py b/headroom/proxy/loop_callback_failure_policy.py new file mode 100644 index 000000000..a802991e3 --- /dev/null +++ b/headroom/proxy/loop_callback_failure_policy.py @@ -0,0 +1,20 @@ +"""Classification policy for event-loop callback failures.""" + +from __future__ import annotations + +from typing import Any + +KNOWN_WEBSOCKET_CALLBACK_MESSAGE = ( + "Exception in callback Connection.connection_lost(ConnectionResetError())" +) +KNOWN_WEBSOCKET_CALLBACK_EXCEPTION = "'ClientConnection' object has no attribute 'recv_messages'" + + +def is_known_websocket_callback_failure(context: dict[str, Any]) -> bool: + """Return True iff this exact websockets callback failure shape is observed.""" + if context.get("message") != KNOWN_WEBSOCKET_CALLBACK_MESSAGE: + return False + exception = context.get("exception") + return isinstance(exception, AttributeError) and str(exception) == ( + KNOWN_WEBSOCKET_CALLBACK_EXCEPTION + ) diff --git a/headroom/proxy/server.py b/headroom/proxy/server.py index d173e43bb..ce7e8a163 100644 --- a/headroom/proxy/server.py +++ b/headroom/proxy/server.py @@ -140,6 +140,7 @@ from headroom.proxy.helpers import ( jitter_delay_ms, retry_after_ms, ) +from headroom.proxy.loop_callback_failure_policy import is_known_websocket_callback_failure from headroom.proxy.loopback_guard import is_loopback_host from headroom.proxy.memory_handler import MemoryConfig, MemoryHandler @@ -2007,18 +2008,7 @@ def _request_is_loopback(request: Request) -> bool: return is_loopback_host(client_host) and is_loopback_host_header(host_header) -def _is_known_websocket_callback_failure(context: dict[str, Any]) -> bool: - """Return True iff this exact websockets callback failure shape is observed.""" - if ( - context.get("message") - != "Exception in callback Connection.connection_lost(ConnectionResetError())" - ): - return False - exception = context.get("exception") - return ( - isinstance(exception, AttributeError) - and str(exception) == "'ClientConnection' object has no attribute 'recv_messages'" - ) +_is_known_websocket_callback_failure = is_known_websocket_callback_failure _tool_schema_saved_from_tags = tool_schema_saved_from_tags diff --git a/tests/test_loop_callback_failure_policy.py b/tests/test_loop_callback_failure_policy.py new file mode 100644 index 000000000..f82f4a371 --- /dev/null +++ b/tests/test_loop_callback_failure_policy.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from headroom.proxy.loop_callback_failure_policy import ( + KNOWN_WEBSOCKET_CALLBACK_EXCEPTION, + KNOWN_WEBSOCKET_CALLBACK_MESSAGE, + is_known_websocket_callback_failure, +) + + +def test_known_websocket_callback_failure_matches_exact_shape() -> None: + assert is_known_websocket_callback_failure( + { + "message": KNOWN_WEBSOCKET_CALLBACK_MESSAGE, + "exception": AttributeError(KNOWN_WEBSOCKET_CALLBACK_EXCEPTION), + } + ) + + +def test_known_websocket_callback_failure_rejects_other_message() -> None: + assert not is_known_websocket_callback_failure( + { + "message": "Exception in callback something_else", + "exception": AttributeError(KNOWN_WEBSOCKET_CALLBACK_EXCEPTION), + } + ) + + +def test_known_websocket_callback_failure_rejects_other_exception() -> None: + assert not is_known_websocket_callback_failure( + { + "message": KNOWN_WEBSOCKET_CALLBACK_MESSAGE, + "exception": AttributeError("different attribute"), + } + ) + assert not is_known_websocket_callback_failure( + { + "message": KNOWN_WEBSOCKET_CALLBACK_MESSAGE, + "exception": RuntimeError(KNOWN_WEBSOCKET_CALLBACK_EXCEPTION), + } + )