PrefectHQ/fastmcp · error · SSRFFetchError
HTTP {response.status_code} fetching {url}
Error message
HTTP {response.status_code} fetching {url} What it means
ssrf_safe_fetch_response raises SSRFFetchError when the HTTP response status code is not in the allowed set (default {200}; callers may widen via allowed_status_codes). Because redirects are not followed (follow_redirects=False), 3xx responses also trigger this error unless explicitly allowed.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/ssrf.py:495
# normal default (True). Proxy-trust mode sets an explicit
# proxy_url and turns trust_env off, so httpx2 has no environment
# -based routing decision left to make — see validate_url() above
# for why that matters.
proxy=target.proxy_url,
trust_env=target.proxy_url is None,
) as client,
client.stream(
"GET",
target.url,
headers=headers,
extensions=extensions,
) as response,
):
if time.monotonic() - start_time > overall_timeout:
raise SSRFFetchError(f"Overall timeout exceeded: {url}")
if response.status_code not in expected_statuses:
raise SSRFFetchError(f"HTTP {response.status_code} fetching {url}")
# Check Content-Length header first if available
content_length = response.headers.get("content-length")
if content_length:
try:
size = int(content_length)
if size > max_size:
raise SSRFFetchError(
f"Response too large: {size} bytes (max {max_size})"
)
except ValueError:
pass
# Stream the response and enforce size limit during download
chunks = []
total = 0
async for chunk in response.aiter_bytes():
if time.monotonic() - start_time > overall_timeout:View on GitHub (pinned to 1f02114297)
Solutions
- Verify the URL is the final, canonical resource location (no redirect chain).
- Widen acceptance with allowed_status_codes={200, 301, 302} only if you also handle the Location manually — redirect following is intentionally off for SSRF safety.
- Pass required credentials via request_headers (note: Host header is stripped).
- If fetching a CIMD client_id, confirm the metadata document exists at the issuer URL exactly.
- Catch SSRFFetchError, inspect the str() for the status code, and surface a clear message to the user.
Example fix
// before
resp = await ssrf_safe_fetch_response(url) # 404 -> SSRFFetchError
// after
resp = await ssrf_safe_fetch_response(url, allowed_status_codes={200, 404})
if resp.status_code == 404:
return None # metadata not published Defensive patterns
Strategy: try-catch
Validate before calling
# pre-check the endpoint out-of-band
code = (await client.head(url, follow_redirects=False)).status_code
if code not in (200,):
raise RuntimeError(f"endpoint returned {code}, not 200") Try / catch
try:
resp = await ssrf_safe_fetch_response(url)
except SSRFFetchError as e:
m = re.match(r"HTTP (\d+)", str(e))
if m:
status = int(m.group(1))
# handle 404 / redirect specifically
else:
raise Prevention
- Use canonical, non-redirecting URLs for metadata endpoints
- Pass allowed_status_codes only when you explicitly handle alternates
- Send required auth via request_headers (Host is stripped)
- Test the exact URL with curl -I before wiring it in
When it happens
Trigger: Target returns 301/302/307 redirect (redirects are disabled by design); returns 404/410 for a missing CIMD metadata document; returns 403/401 for auth-protected resources; or any non-200 when the caller did not pass allowed_status_codes.
Common situations: Fetching OAuth client metadata at a URL that redirects to a canonical domain; document not published at the exact issuer URL; server requiring an Authorization header the caller omitted from request_headers.
Related errors
- Response too large: {size} bytes (max {max_size})
- Unexpected authorization response: {response.status_code}
- {str(e) from SSRFError/SSRFFetchError}
- str(e)
- OIDC discovery for issuer {issuer!r} failed: {e}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/44edae49e9fa762f.
Report an issue: GitHub.