dify-plugin-tools-mcp_sse icon indicating copy to clipboard operation
dify-plugin-tools-mcp_sse copied to clipboard

httpx.Client timeout configuration issue causes SSE connection failures

Open Masa1984a opened this issue 6 months ago • 0 comments

Please confirm before submission 在提交之前,请确认

Dify version Dify版本

1.4.1

Plugin version 插件版本

0.1.10

HTTP with SSE or Streamable HTTP

HTTP with SSE

Problem description 问题描述

Summary

The MCP SSE client experiences timeout failures within approximately 5 seconds when connecting to slower MCP servers, even when timeout and sse_read_timeout are configured to higher values (e.g., 50+ seconds). This issue occurs because the httpx.Client is initialized without explicit timeout configuration, causing it to use httpx's default 5-second timeout.

Current Implementation

File: utils/mcp_client.py
Line: 56

def __init__(self, name: str, url: str,
             headers: dict[str, Any] | None = None,
             timeout: float = 50,
             sse_read_timeout: float = 50,
             ):
    self.name = name
    self.url = url
    self.timeout = timeout
    self.sse_read_timeout = sse_read_timeout
    self.endpoint_url = None
    self.client = httpx.Client(headers=headers)  # ← Issue: No timeout specified
    # ... rest of initialization

The timeout and sse_read_timeout parameters are stored but not applied to the httpx.Client initialization. They are only used later in the connect_sse() call:

with connect_sse(
    client=self.client,
    method="GET",
    url=self.url,
    timeout=httpx.Timeout(self.timeout, read=self.sse_read_timeout),
    # ... other parameters
):

Specific Issues Observed

1. Quick Timeout Failures

  • Symptom: SSE connections fail within 5 seconds, regardless of configured timeout values
  • Frequency: Consistently occurs with MCP servers that take longer to:
    • Complete initialization
    • Send the first SSE event
    • Respond due to cold start delays (e.g., cloud containers)

2. Inconsistent Behavior

  • Symptom: Sometimes connections work, sometimes they don't, depending on server response time
  • Impact: Unreliable connections to legitimate MCP servers hosted on cloud platforms

3. Authentication-Related Timing

  • Symptom: Connections with API key authentication appear to fail more frequently
  • Reason: Additional authentication processing time pushes total response time over the 5-second threshold

Root Cause Analysis

Technical Cause

  1. httpx Default Timeout: When httpx.Client() is initialized without explicit timeout configuration, it uses httpx's default timeout of 5 seconds for both connect and read operations
  2. SSE Connection Flow: The initial SSE connection establishment happens through the client's default timeout, not the connect_sse() timeout override
  3. First Event Dependency: SSE connections must receive the first event within the client's default timeout, or the connection is terminated

Evidence

  • httpx documentation specifies default timeout behavior
  • The timeout parameter in plugin configuration affects only the connect_sse() call, not the underlying client
  • Cloud-hosted MCP servers often require 5+ seconds for cold start and initial response

Proposed Solution

1. Apply Configuration Timeout to httpx.Client

Recommended Change:

def __init__(self, name: str, url: str,
             headers: dict[str, Any] | None = None,
             timeout: float = 50,
             sse_read_timeout: float = 50,
             ):
    self.name = name
    self.url = url
    self.timeout = timeout
    self.sse_read_timeout = sse_read_timeout
    self.endpoint_url = None
    
    # Apply timeout configuration to the client
    client_timeout = httpx.Timeout(
        connect=min(timeout, 30.0),  # Reasonable connect timeout
        read=sse_read_timeout,       # Use configured read timeout
        write=30.0,                  # Reasonable write timeout
        pool=10.0                    # Pool timeout
    )
    self.client = httpx.Client(headers=headers, timeout=client_timeout)
    # ... rest of initialization

2. Alternative Minimal Fix

If the above change is too complex, a minimal fix would be:

self.client = httpx.Client(
    headers=headers,
    timeout=httpx.Timeout(connect=30.0, read=max(timeout, 30.0))
)

3. Backward Compatibility

  • Keep the current timeout and sse_read_timeout parameters
  • Ensure the connect_sse() call can still override these values if needed
  • Consider adding validation to prevent extremely short timeouts

Expected Benefits

  1. Reliability: Connections to slower MCP servers will succeed consistently
  2. User Experience: Plugin configuration timeouts will work as expected
  3. Cloud Compatibility: Better support for cloud-hosted MCP servers with cold start delays
  4. Consistency: Timeout behavior will match user expectations and documentation

Test Scenarios

To verify the fix:

  1. Connect to an MCP server that responds slowly (6+ seconds)
  2. Configure plugin with timeout: 60, sse_read_timeout: 60
  3. Verify connection succeeds instead of timing out at 5 seconds

References

Masa1984a avatar Jun 01 '25 00:06 Masa1984a