From ad59d553386b04417351c272586063eca36616b0 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 6 Aug 2026 11:23:22 -0400 Subject: [PATCH] fix(tools): bound the exception text dispatch writes into its own log line dispatch() called logger.exception with the exception interpolated into the message. exc_info renders the same exception again in the traceback, so a failing tool wrote its error body to the log twice. Every tool exception passes through this one handler, so a large HTTP error body from any tool landed here at full size. Bound the message copy. The traceback still renders the exception once, which is what an operator needs to place the failure. Same double-write @arimu1 fixed in the vision, image, and TTS handlers in #75938. --- tests/tools/test_registry.py | 21 +++++++++++++++++++++ tools/registry.py | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_registry.py b/tests/tools/test_registry.py index 6cefab0fc7bd5..6573941fdafc8 100644 --- a/tests/tools/test_registry.py +++ b/tests/tools/test_registry.py @@ -237,6 +237,27 @@ class TestDispatchBoundsDirectErrorResults: assert reg.dispatch("nested", {}) == payload +class TestDispatchExceptionLogging: + def test_raising_handler_logs_bounded_message(self, caplog): + import logging + body = "upstream said: " + "Q" * 200_000 + reg = ToolRegistry() + reg.register( + name="boom", + toolset="core", + schema=_make_schema("boom"), + handler=lambda args, **kw: (_ for _ in ()).throw(RuntimeError(body)), + ) + with caplog.at_level(logging.ERROR, logger="tools.registry"): + result = json.loads(reg.dispatch("boom", {})) + messages = [r.getMessage() for r in caplog.records] + assert messages, "dispatch should log the failure" + for message in messages: + assert len(message) < _MAX_LOGGED_ERROR_CHARS + 200 + assert body not in message + assert len(result["error"]) < _MAX_TOOL_ERROR_CHARS + 200 + + class TestToolsetAvailability: def test_no_check_fn_is_available(self): reg = ToolRegistry() diff --git a/tools/registry.py b/tools/registry.py index 333922d2d0fbb..fc5f98ec23bf8 100644 --- a/tools/registry.py +++ b/tools/registry.py @@ -818,7 +818,10 @@ class ToolRegistry: result = entry.handler(args, **kwargs) return self._normalize_handler_result(name, result) except Exception as e: - logger.exception("Tool %s dispatch error: %s", name, e) + # exc_info already renders the exception, so keep the message copy bounded. + logger.exception( + "Tool %s dispatch error: %s", name, _bound_error_text(str(e)) + ) # Route through the sanitizer so framing tokens / CDATA / fences # in exception strings don't reach the model as structural noise. # See model_tools._sanitize_tool_error for rationale.