Fix HTTP transport timeout defaulting to 5 seconds (#2849)

This commit is contained in:
Jeremiah Lowin 2026-01-12 14:47:21 -05:00 committed by GitHub
commit 9b18db2379
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 14 deletions

View file

@ -296,7 +296,7 @@ max_lines = 713
[[rules]]
path = "src/fastmcp/client/transports.py"
max_lines = 1186
max_lines = 1205
[[rules]]
path = "docs/docs.json"

View file

@ -25,7 +25,7 @@ from mcp.client.sse import sse_client
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
@ -208,6 +208,19 @@ class StreamableHttpTransport(ClientTransport):
sse_read_timeout: datetime.timedelta | float | int | None = None,
httpx_client_factory: McpHttpClientFactory | None = None,
):
"""Initialize a Streamable HTTP transport.
Args:
url: The MCP server endpoint URL.
headers: Optional headers to include in requests.
auth: Authentication method - httpx.Auth, "oauth" for OAuth flow,
or a bearer token string.
sse_read_timeout: Deprecated. Use read_timeout_seconds in session_kwargs.
httpx_client_factory: Optional factory for creating httpx.AsyncClient.
If provided, must accept keyword arguments: headers, auth,
follow_redirects, and optionally timeout. Using **kwargs is
recommended to ensure forward compatibility.
"""
if isinstance(url, AnyUrl):
url = str(url)
if not isinstance(url, str) or not url.startswith("http"):
@ -253,25 +266,31 @@ class StreamableHttpTransport(ClientTransport):
# 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, # type: ignore[call-arg]
**({"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 (

View file

@ -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})
assert result.data == "Completed in 6 seconds"