Skip to content

fix: preserve timeout message through BaseExceptionGroup unwrapping#4713

Open
shoummu1 wants to merge 2 commits into
mainfrom
fix/2782-preserve-timeout-message-exceptiongroup
Open

fix: preserve timeout message through BaseExceptionGroup unwrapping#4713
shoummu1 wants to merge 2 commits into
mainfrom
fix/2782-preserve-timeout-message-exceptiongroup

Conversation

@shoummu1
Copy link
Copy Markdown
Collaborator

Bug-fix PR

📌 Summary

When a tool invocation times out via StreamableHTTP or SSE transport, the error
response returned to the client contains an empty message ("Tool invocation failed: ").
The descriptive timeout message is generated correctly but lost during exception
propagation through Python 3.11+ BaseExceptionGroup wrapping.

🔗 Related Issue

Closes: #2782

🔁 Reproduction Steps

See #2781 and #2782.

  1. Register an MCP server reachable via StreamableHTTP or SSE transport.
  2. Set a short tool_timeout so the tool times out reliably.
  3. Invoke the tool — the client receives "Tool invocation failed: " (empty message).

🐞 Root Cause

tool_service.py raises ToolTimeoutError("Tool invocation timed out after {N}s")
inside nested async with context managers (streamablehttp_client, ClientSession).
During __aexit__ cleanup the MCP SDK's internal TaskGroup may raise additional
exceptions from cancelled tasks, causing Python 3.11+ to wrap everything in a
BaseExceptionGroup.

In the outer invoke_tool handler:

  1. except ToolTimeoutError does not match a BaseExceptionGroup containing a ToolTimeoutError.
  2. Falls through to except BaseException.
  3. The unwrapping loop (exceptions[0]) finds the bare asyncio.TimeoutError — which carries no message.
  4. str(asyncio.TimeoutError())"" → the client sees an empty error message.

The same silent-message loss affected the structured logger in the SSE and
StreamableHTTP inner handlers (logged error_message: "" instead of the timeout
description).

💡 Fix Description

After unwrapping the BaseExceptionGroup root cause, add a fallback that
re-attaches the descriptive timeout message whenever the root cause is a bare
TimeoutError / asyncio.TimeoutError with an empty string representation:

error_message = str(root_cause)
# Preserve timeout context when message is lost during ExceptionGroup wrapping
if not error_message and isinstance(root_cause, (TimeoutError, asyncio.TimeoutError)):
    error_message = f"Tool invocation timed out after {effective_timeout}s"

Applied to three locations:

File Line Path Variable
tool_service.py ~5457 SSE inner handler root_cause_message (logging)
tool_service.py ~5647 StreamableHTTP inner handler root_cause_message (logging)
tool_service.py ~5961 Outer invoke_tool handler error_message (returned to client)

The fallback is only triggered when the message is empty and the root cause
is a TimeoutError subclass, so normal non-timeout errors are completely
unaffected.

🧪 Verification

A regression test was added to tests/unit/mcpgateway/services/test_tool_service.py:

test_invoke_tool_timeout_message_preserved_through_exception_group

It constructs a nested ExceptionGroup(ExceptionGroup(TimeoutError())), asserts the raised
ToolInvocationError contains "timed out after", and verifies the metrics buffer records
the correct non-empty error_message.

Check Command Status
Lint suite make lint Passed
Unit tests make test Passed
Coverage ≥ 90 % make coverage Passed

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • Regression test added (test_invoke_tool_timeout_message_preserved_through_exception_group)
  • No secrets/credentials committed

…timeout ExceptionGroup fallback

Signed-off-by: Shoumi <[email protected]>
@shoummu1 shoummu1 force-pushed the fix/2782-preserve-timeout-message-exceptiongroup branch from 63973b5 to 03755ba Compare May 13, 2026 06:09
@brian-hussey brian-hussey added the ica ICA related issues label May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ica ICA related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ENHANCEMENT][OBSERVABILITY]: Preserve timeout error message through ExceptionGroup unwrapping in tool invocations

2 participants