mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Fix HTTP transport timeout defaulting to 5 seconds (#2849)
This commit is contained in:
parent
18c71f9f34
commit
9b18db2379
3 changed files with 84 additions and 14 deletions
2
loq.toml
2
loq.toml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
51
tests/integration_tests/test_timeout_fix.py
Normal file
51
tests/integration_tests/test_timeout_fix.py
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue