PrefectHQ/fastmcp · error · SSRFFetchError

Overall timeout exceeded: {url}

Error message

Overall timeout exceeded: {url}

What it means

ssrf_safe_fetch_response raises SSRFFetchError when the total wall-clock time across all fetch targets (one per pinned DNS-resolved IP) exceeds the overall_timeout budget (default 30s). The check runs before starting each target's HTTP request so a slow multi-IP host cannot extend the fetch indefinitely. It bounds the whole operation, not a single connection.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/ssrf.py:442

    This is equivalent to :func:`ssrf_safe_fetch` but returns response headers
    and status code, and supports conditional request headers.
    """
    start_time = time.monotonic()

    # Validate URL and resolve DNS
    validated = await validate_url(url, require_path=require_path)

    last_error: Exception | None = None
    expected_statuses = allowed_status_codes or {200}

    # One target per pinned IP in default mode; a single unpinned target in proxy mode.
    targets = _build_fetch_targets(validated)

    for target in targets:
        elapsed = time.monotonic() - start_time
        if elapsed > overall_timeout:
            raise SSRFFetchError(f"Overall timeout exceeded: {url}")
        remaining = max(1.0, overall_timeout - elapsed)

        logger.debug("SSRF-safe fetch: %s -> %s", url, target.url)

        # In pinned mode Host is forced to the validated hostname; in proxy mode httpx
        # derives it from the hostname URL. Either way, never let a caller override it.
        headers: dict[str, str] = {}
        if target.host_header is not None:
            headers["Host"] = target.host_header
        if request_headers:
            for key, value in request_headers.items():
                if key.lower() == "host":
                    continue
                headers[key] = value

        # Pin SNI to the hostname when connecting to an IP literal; in proxy mode httpx
        # derives SNI from the URL, so no override is sent.
        extensions: dict[str, str] = {}

View on GitHub (pinned to 1f02114297)

Solutions

  1. Raise the overall_timeout parameter (e.g. overall_timeout=60) to cover N resolved IPs x per-request timeout.
  2. Check network reachability of the host's resolved IPs (firewall/security-group rules dropping packets cause full connect timeouts).
  3. Increase the per-request timeout floor or reduce reachable IPs so the budget is not consumed before the last target.
  4. If the endpoint is legitimately slow, fetch it outside the SSRF-guarded path only after validating the host is trusted.
  5. Catch SSRFFetchError and fall back to a cached/last-known-good copy of the resource.

Example fix

// before
content = await ssrf_safe_fetch(url)  # default overall_timeout=30s, multi-IP host
// after
content = await ssrf_safe_fetch(url, timeout=10.0, overall_timeout=90.0)
Defensive patterns

Strategy: retry

Validate before calling

# sanity-check budget vs expected IP count
ips = socket.getaddrinfo(host, None)
budget = min(len(ips), 8) * 10.0  # per-target timeout x target count
assert budget <= overall_timeout, "increase overall_timeout"

Try / catch

try:
    content = await ssrf_safe_fetch(url, overall_timeout=90.0)
except SSRFFetchError as e:
    if "Overall timeout" in str(e):
        await asyncio.sleep(backoff)
        content = await ssrf_safe_fetch(url, overall_timeout=90.0)
    else:
        raise

Prevention

When it happens

Trigger: Calling ssrf_safe_fetch()/fetch() on a URL whose hostname resolves to multiple IPs where earlier targets each consume close to the per-request timeout (default 10s), so elapsed time before a later target exceeds overall_timeout; also raised after the response headers arrive (line 492) or mid-stream (line 514) if the budget expires. Common with unreachable-but-not-refusing hosts (firewall DROP causing connect timeouts) that have several A/AAAA records.

Common situations: Fetching OAuth metadata (CIMD) from a server behind a security group that drops packets; IPv6 addresses tried first that black-hole until timeout; very slow upstream; overall_timeout left at default while the per-target timeouts add up across many resolved IPs.

Understand the failure class

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/75e9e10eb004f0a5. Report an issue: GitHub.