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

  1. curl -s <jwks_uri> | head — verify the body is JSON starting with '{"keys"'.
  2. 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).
  3. Remove or reconfigure any proxy/WAF that returns HTML challenge pages with 200 status for API paths.
  4. 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

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


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