PrefectHQ/fastmcp · error · ValueError
Invalid JWKS JSON: {e}
Error message
Invalid JWKS JSON: {e} What it means
This ValueError means the JWKS endpoint returned a 2xx response whose body is not valid JSON — json.loads/json parsing raised JSONDecodeError. The library raises it so token verification fails closed instead of crashing with a raw parse error. It only occurs on the standard (non-SSRF) fetch path via response.json(), or via json.loads in ssrf mode.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/providers/jwt.py:440
return self._jwks_cache[kid]
else:
# No kid in token - only allow if there's exactly one key
if len(self._jwks_cache) == 1:
return next(iter(self._jwks_cache.values()))
elif len(self._jwks_cache) > 1:
raise ValueError(
"Multiple keys in JWKS but no key ID (kid) in token"
)
else:
raise ValueError("No keys found in JWKS")
except (SSRFError, SSRFFetchError) as e:
self.logger.debug("JWKS fetch blocked by SSRF protection: %s", e)
raise ValueError(f"Failed to fetch JWKS: {e}") from e
except httpx2.HTTPError as e:
raise ValueError(f"Failed to fetch JWKS: {e}") from e
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JWKS JSON: {e}") from e
except (JoseError, TypeError, KeyError, ValueError) as e:
self.logger.debug("JWKS key processing failed: %s", e)
raise ValueError(f"Failed to process JWKS: {e}") from e
async def _fetch_jwks(self) -> dict[str, Any]:
"""Fetch JWKS data, using SSRF-safe or standard fetch based on config."""
if not self.jwks_uri:
raise ValueError("JWKS URI not configured")
if self.ssrf_safe:
content = await ssrf_safe_fetch(
self.jwks_uri,
max_size=65536,
timeout=10.0,
overall_timeout=30.0,
)
return json.loads(content)
else:View on GitHub (pinned to 1f02114297)
Solutions
- curl -s <jwks_uri> | head — verify the body is JSON starting with '{"keys"'.
- Correct jwks_uri to the real JWKS endpoint (e.g. https://idp/realms/<realm>/protocol/openid-connect/certs or https://<domain>/.well-known/jwks.json).
- Remove or reconfigure any proxy/WAF that returns HTML challenge pages with 200 status for API paths.
- Check that the endpoint isn't returning an empty 200 (some misconfigured gateways do) — the server must send a JSON key set.
Example fix
// before
jwks_uri = "https://idp.example.com" # returns HTML homepage
// after
jwks_uri = "https://idp.example.com/.well-known/jwks.json" # returns {"keys": [...]} Defensive patterns
Strategy: validation
Validate before calling
import httpx, json
async def jwks_returns_json(uri: str) -> bool:
try:
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
r = await client.get(uri)
r.raise_for_status()
data = r.json()
return isinstance(data, dict) and isinstance(data.get("keys"), list)
except (httpx.HTTPError, json.JSONDecodeError):
return False
# run against the configured jwks_uri before deploying Type guard
def looks_like_jwks(payload: object) -> bool:
return isinstance(payload, dict) and isinstance(payload.get("keys"), list) and len(payload["keys"]) > 0 Try / catch
try:
claims = await verifier.verify_token(token)
except ValueError as e:
if "Invalid JWKS JSON" in str(e):
logger.error("JWKS endpoint returned non-JSON (proxy/login page?): %s", e)
return None Prevention
- Confirm the body starts with {"keys": [...] via curl before configuring.
- Exclude the JWKS path from SSO redirects, WAF challenges, and HTML error pages.
- Use the exact well-known certs URL from the IdP docs, not the issuer root.
- Set the Accept: application/json header at any proxy in front of the IdP.
When it happens
Trigger: verify_token -> _get_jwks_key -> _fetch_jwks when the endpoint replies 200 with HTML (e.g. a login/redirect page, SPA index, or error page), an empty body, or truncated output; common when jwks_uri points at the issuer root or an auth page rather than the certs endpoint.
Common situations: jwks_uri copy-pasted incorrectly (missing the /certs or /.well-known path); reverse proxy or WAF returning an HTML interstitial (SSO login, Cloudflare challenge) with status 200; corporate proxy injecting a block page; IdP misconfigured behind a redirect that lands on HTML.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- Failed to extract key ID from token: {e}
- Assertion JOSE header must be a JSON object
- Assertion payload is not a JSON object
- JWKS URI not configured
- Key ID '{kid}' found in JWKS but its key type is unsupported
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/d05da9320a13fdff.
Report an issue: GitHub.