Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions src/fastmcp/client/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamable_http_client
from mcp.server.fastmcp import FastMCP as FastMCP1Server
from mcp.shared._httpx_utils import McpHttpClientFactory
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
from mcp.shared.memory import create_client_server_memory_streams
from pydantic import AnyUrl
from typing_extensions import TypedDict, Unpack
Expand Down Expand Up @@ -284,25 +284,31 @@ async def connect_session(
# need to be forwarded to the remote server.
headers = get_http_headers() | self.headers

# Build httpx client configuration
httpx_client_kwargs: dict[str, Any] = {
"headers": headers,
"auth": self.auth,
"follow_redirects": True,
}

# Configure timeout if provided (convert timedelta to seconds for httpx)
# Configure timeout if provided, preserving MCP's 30s connect default
timeout: httpx.Timeout | None = None
if session_kwargs.get("read_timeout_seconds") is not None:
read_timeout_seconds = cast(
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
)
httpx_client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
timeout = httpx.Timeout(30.0, read=read_timeout_seconds.total_seconds())

# Create httpx client from factory or use default
# Create httpx client from factory or use default with MCP-appropriate timeouts
# create_mcp_http_client uses 30s connect/5min read timeout by default,
# and always enables follow_redirects
if self.httpx_client_factory is not None:
http_client = self.httpx_client_factory(**httpx_client_kwargs)
# Factory clients get the full kwargs for backwards compatibility
http_client = self.httpx_client_factory(
headers=headers,
auth=self.auth,
follow_redirects=True,
**({"timeout": timeout} if timeout else {}),
)
else:
http_client = httpx.AsyncClient(**httpx_client_kwargs)
http_client = create_mcp_http_client(
headers=headers,
timeout=timeout,
auth=self.auth,
)

# Ensure httpx client is closed after use
async with (
Expand Down
51 changes: 51 additions & 0 deletions tests/integration_tests/test_timeout_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Test that verifies the timeout fix for issue #2842 and #2845."""

import asyncio

import pytest

from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.utilities.tests import run_server_async


def create_test_server() -> FastMCP:
"""Create a FastMCP server with a slow tool."""
server = FastMCP("TestServer")

@server.tool
async def slow_tool(duration: int = 6) -> str:
"""A tool that takes some time to complete."""
await asyncio.sleep(duration)
return f"Completed in {duration} seconds"

return server


@pytest.fixture
async def streamable_http_server():
"""Start a test server and return its URL."""
server = create_test_server()
async with run_server_async(server) as url:
yield url


@pytest.mark.integration
@pytest.mark.timeout(15)
async def test_slow_tool_with_http_transport(streamable_http_server: str):
"""Test that tools taking >5 seconds work correctly with HTTP transport.

This test verifies the fix for:
- Issue #2842: Client can't get result after upgrading to 2.14.2
- Issue #2845: Server doesn't return results when tool takes >5 seconds

The root cause was that the httpx client was created without explicit
timeout configuration, defaulting to httpx's 5-second timeout.
"""
async with Client(
transport=StreamableHttpTransport(streamable_http_server)
) as client:
# This should NOT timeout since we fixed the default timeout
result = await client.call_tool("slow_tool", {"duration": 6})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep test runtime under pytest 5s timeout

This test forces a 6-second tool run (duration=6), but pytest-timeout is globally set to 5 seconds in pyproject.toml ([tool.pytest.ini_options].timeout = 5). As written, the test will be terminated by pytest-timeout before the tool completes, so CI will fail even when the HTTP fix works. Use a shorter duration or add a per-test timeout marker to keep it within the configured limit.

Useful? React with 👍 / 👎.

assert result.data == "Completed in 6 seconds"