{"record":{"id":"75e9e10eb004f0a5","repo":"PrefectHQ/fastmcp","slug":"overall-timeout-exceeded-url","errorCode":null,"errorMessage":"Overall timeout exceeded: {url}","messagePattern":"Overall timeout exceeded: (.+?)","errorType":"exception","errorClass":"SSRFFetchError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/ssrf.py","lineNumber":442,"sourceCode":"\n    This is equivalent to :func:`ssrf_safe_fetch` but returns response headers\n    and status code, and supports conditional request headers.\n    \"\"\"\n    start_time = time.monotonic()\n\n    # Validate URL and resolve DNS\n    validated = await validate_url(url, require_path=require_path)\n\n    last_error: Exception | None = None\n    expected_statuses = allowed_status_codes or {200}\n\n    # One target per pinned IP in default mode; a single unpinned target in proxy mode.\n    targets = _build_fetch_targets(validated)\n\n    for target in targets:\n        elapsed = time.monotonic() - start_time\n        if elapsed > overall_timeout:\n            raise SSRFFetchError(f\"Overall timeout exceeded: {url}\")\n        remaining = max(1.0, overall_timeout - elapsed)\n\n        logger.debug(\"SSRF-safe fetch: %s -> %s\", url, target.url)\n\n        # In pinned mode Host is forced to the validated hostname; in proxy mode httpx\n        # derives it from the hostname URL. Either way, never let a caller override it.\n        headers: dict[str, str] = {}\n        if target.host_header is not None:\n            headers[\"Host\"] = target.host_header\n        if request_headers:\n            for key, value in request_headers.items():\n                if key.lower() == \"host\":\n                    continue\n                headers[key] = value\n\n        # Pin SNI to the hostname when connecting to an IP literal; in proxy mode httpx\n        # derives SNI from the URL, so no override is sent.\n        extensions: dict[str, str] = {}","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/ssrf.py#L424-L460","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Raise the overall_timeout parameter (e.g. overall_timeout=60) to cover N resolved IPs x per-request timeout.","Check network reachability of the host's resolved IPs (firewall/security-group rules dropping packets cause full connect timeouts).","Increase the per-request timeout floor or reduce reachable IPs so the budget is not consumed before the last target.","If the endpoint is legitimately slow, fetch it outside the SSRF-guarded path only after validating the host is trusted.","Catch SSRFFetchError and fall back to a cached/last-known-good copy of the resource."],"exampleFix":"// before\ncontent = await ssrf_safe_fetch(url)  # default overall_timeout=30s, multi-IP host\n// after\ncontent = await ssrf_safe_fetch(url, timeout=10.0, overall_timeout=90.0)","handlingStrategy":"retry","validationCode":"# sanity-check budget vs expected IP count\nips = socket.getaddrinfo(host, None)\nbudget = min(len(ips), 8) * 10.0  # per-target timeout x target count\nassert budget <= overall_timeout, \"increase overall_timeout\"","typeGuard":null,"tryCatchPattern":"try:\n    content = await ssrf_safe_fetch(url, overall_timeout=90.0)\nexcept SSRFFetchError as e:\n    if \"Overall timeout\" in str(e):\n        await asyncio.sleep(backoff)\n        content = await ssrf_safe_fetch(url, overall_timeout=90.0)\n    else:\n        raise","preventionTips":["Set overall_timeout >= (number of resolved IPs) x per-request timeout","Prefer REJECT-style failure over DROP on firewalls so failures return fast","Monitor fetch latency and alert when elapsed approaches the budget"],"tags":["network","timeout","ssrf","httpx"],"backgroundTag":"request-timeout","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}