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
- 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.
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
- 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
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timeout fetching {url}
- Error fetching {url}: {last_error}
- 408
- User server did not start on port {mcp_port}
- {str(e) from SSRFError/SSRFFetchError}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/75e9e10eb004f0a5.
Report an issue: GitHub.