{"record":{"id":"d05da9320a13fdff","repo":"PrefectHQ/fastmcp","slug":"invalid-jwks-json-e","errorCode":null,"errorMessage":"Invalid JWKS JSON: {e}","messagePattern":"Invalid JWKS JSON: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/providers/jwt.py","lineNumber":440,"sourceCode":"                return self._jwks_cache[kid]\n            else:\n                # No kid in token - only allow if there's exactly one key\n                if len(self._jwks_cache) == 1:\n                    return next(iter(self._jwks_cache.values()))\n                elif len(self._jwks_cache) > 1:\n                    raise ValueError(\n                        \"Multiple keys in JWKS but no key ID (kid) in token\"\n                    )\n                else:\n                    raise ValueError(\"No keys found in JWKS\")\n\n        except (SSRFError, SSRFFetchError) as e:\n            self.logger.debug(\"JWKS fetch blocked by SSRF protection: %s\", e)\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except httpx2.HTTPError as e:\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except json.JSONDecodeError as e:\n            raise ValueError(f\"Invalid JWKS JSON: {e}\") from e\n        except (JoseError, TypeError, KeyError, ValueError) as e:\n            self.logger.debug(\"JWKS key processing failed: %s\", e)\n            raise ValueError(f\"Failed to process JWKS: {e}\") from e\n\n    async def _fetch_jwks(self) -> dict[str, Any]:\n        \"\"\"Fetch JWKS data, using SSRF-safe or standard fetch based on config.\"\"\"\n        if not self.jwks_uri:\n            raise ValueError(\"JWKS URI not configured\")\n\n        if self.ssrf_safe:\n            content = await ssrf_safe_fetch(\n                self.jwks_uri,\n                max_size=65536,\n                timeout=10.0,\n                overall_timeout=30.0,\n            )\n            return json.loads(content)\n        else:","sourceCodeStart":422,"sourceCodeEnd":458,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/providers/jwt.py#L422-L458","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\njwks_uri = \"https://idp.example.com\"  # returns HTML homepage\n// after\njwks_uri = \"https://idp.example.com/.well-known/jwks.json\"  # returns {\"keys\": [...]}","handlingStrategy":"validation","validationCode":"import httpx, json\n\nasync def jwks_returns_json(uri: str) -> bool:\n    try:\n        async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:\n            r = await client.get(uri)\n            r.raise_for_status()\n            data = r.json()\n            return isinstance(data, dict) and isinstance(data.get(\"keys\"), list)\n    except (httpx.HTTPError, json.JSONDecodeError):\n        return False\n\n# run against the configured jwks_uri before deploying","typeGuard":"def looks_like_jwks(payload: object) -> bool:\n    return isinstance(payload, dict) and isinstance(payload.get(\"keys\"), list) and len(payload[\"keys\"]) > 0","tryCatchPattern":"try:\n    claims = await verifier.verify_token(token)\nexcept ValueError as e:\n    if \"Invalid JWKS JSON\" in str(e):\n        logger.error(\"JWKS endpoint returned non-JSON (proxy/login page?): %s\", e)\n        return None","preventionTips":["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."],"tags":["json","http","jwt","jwks"],"backgroundTag":"invalid-json-response","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}