unclecode/crawl4ai · error · HTTPException

upstream request timed out

Error message

upstream request timed out

What it means

The MCP bridge wraps upstream calls with an httpx.AsyncClient(timeout=timeout); when the upstream exceeds that timeout, httpx.TimeoutException is caught and re-raised as HTTPException(504, 'upstream request timed out'). The timeout comes from attach_mcp(timeout=...); None means no limit, so the 504 only fires when a finite timeout was configured.

Source

Thrown at deploy/docker/mcp_bridge.py:81

                path = path.replace(placeholder, str(v))
                kwargs.pop(k)
        url = base_url.rstrip("/") + path

        headers = _service_auth_headers()
        async with httpx.AsyncClient(timeout=timeout) as client:
            try:
                r = (
                    await client.get(url, params=kwargs, headers=headers)
                    if method == "GET"
                    else await client.request(method, url, json=kwargs, headers=headers)
                )
                r.raise_for_status()
                return r.text if method == "GET" else r.json()
            except httpx.HTTPStatusError as e:
                # surface FastAPI error details instead of plain 500
                raise HTTPException(e.response.status_code, e.response.text)
            except httpx.TimeoutException:
                raise HTTPException(504, "upstream request timed out")
    return proxy

# ── main entry point ────────────────────────────────────────────
def attach_mcp(
    app: FastAPI,
    *,                          # keyword‑only
    base: str = "/mcp",
    name: str | None = None,
    base_url: str,              # eg. "http://127.0.0.1:8020"
    timeout: float | None = None,  # httpx timeout in seconds; None = no limit
) -> None:
    """Call once after all routes are declared to expose WS+SSE MCP endpoints."""
    server_name = name or app.title or "FastAPI-MCP"
    mcp = Server(server_name)

    # tools: Dict[str, Callable] = {}
    tools: Dict[str, Tuple[Callable, Callable]] = {}
    resources: Dict[str, Callable] = {}

View on GitHub (pinned to 7e80152142)

Solutions

  1. Raise attach_mcp(timeout=...) to exceed the slowest tool's expected duration (or None to disable).
  2. Check upstream service health/latency — fix the slowness rather than only extending the deadline.
  3. For long jobs, switch to async patterns: submit the job via one tool, poll status via another, so each call is short.

Example fix

# before
attach_mcp(app, base_url="http://127.0.0.1:8020", timeout=5.0)

# after
attach_mcp(app, base_url="http://127.0.0.1:8020", timeout=300.0)
Defensive patterns

Strategy: retry

Try / catch

from fastapi import HTTPException

for attempt in range(3):
    try:
        return await proxy(**kwargs)
    except HTTPException as e:
        if e.status_code == 504 and attempt < 2:
            await asyncio.sleep(2 ** attempt)
            continue
        raise

Prevention

When it happens

Trigger: A proxied tool call (e.g. a long crawl or LLM job status poll) taking longer than the attach_mcp timeout value; upstream service slow/hung under load; timeout set aggressively (e.g. 5s) for tools that legitimately take minutes.

Common situations: Default proxy timeouts too small for crawl-class operations; upstream saturated so p95 latency crosses the threshold; network stalls between bridge and base_url service.

Understand the failure class

Related errors


AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14). Data as JSON: /api/errors/2fb6a27e0e296234. Report an issue: GitHub.