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.
This commit is contained in:
Erosika 2026-08-06 11:23:22 -04:00 committed by kshitij
parent 84bc430073
commit ad59d55338
2 changed files with 25 additions and 1 deletions

View File

@ -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()

View File

@ -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.