Skip to content

RetryMiddleware doesn't retry tool errors because FastMCP wraps exceptions before middleware sees them #3855

@strawgate

Description

@strawgate

Description

RetryMiddleware doesn't work for tool calls because FastMCP's call_tool() wraps raw exceptions as ToolError before control returns to the middleware. The middleware's _should_retry() checks isinstance(error, retry_exceptions) but receives ToolError, not the original ConnectionError/TimeoutError.

The ErrorHandlingMiddleware handles this correctly by checking error.__cause__, but RetryMiddleware doesn't.

Reproduction

from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.middleware.error_handling import RetryMiddleware

call_count = 0

mcp = FastMCP('test')
mcp.add_middleware(RetryMiddleware(max_retries=3, base_delay=0.01, retry_exceptions=(ConnectionError,)))

@mcp.tool
def always_fail() -> str:
    global call_count
    call_count += 1
    raise ConnectionError('connection refused')

async with Client(mcp) as client:
    try:
        await client.call_tool('always_fail')
    except:
        pass

print(f'Tool called {call_count} times')
# Prints: 1 (expected 4 with 3 retries)

Root Cause

In call_tool() (server.py line 1169-1185), all non-FastMCPError exceptions are caught and re-raised as ToolError(...) from e. This happens inside the middleware chain, so the middleware receives ToolError with __cause__=ConnectionError. The _should_retry method only checks the top-level error type.

Suggested Fix

Update RetryMiddleware._should_retry to also check error.__cause__:

def _should_retry(self, error: Exception) -> bool:
    if isinstance(error, self.retry_exceptions):
        return True
    if error.__cause__ is not None and isinstance(error.__cause__, self.retry_exceptions):
        return True
    return False

Note: ErrorHandlingMiddleware._transform_error already handles this correctly (line 92: error_type = type(error.__cause__) if error.__cause__ else type(error)).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.potential-duplicateBot-suggested duplicate awaiting human review. Auto-closes after 3 days if unchallenged.serverRelated to FastMCP server implementation or server-side functionality.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions