PrefectHQ/fastmcp · error · OAuthError
OAuth token endpoint error: {token['error']}: {token.get('er
Error message
OAuth token endpoint error: {token['error']}: {token.get('error_description')} What it means
The upstream OAuth provider's token endpoint responded with a JSON body containing an 'error' field (standard RFC 6749 error response). The library wraps it in OAuthError preserving the provider's error code and optional description, e.g. invalid_grant or invalid_client.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/oauth_proxy/upstream.py:94
elif method == "none":
data["client_id"] = self.client_id
else:
raise ValueError(
f"Unsupported token_endpoint_auth_method: {method!r}. "
"Supported methods: client_secret_basic, client_secret_post, none."
)
async def _request_token(self, url: str, data: dict[str, Any]) -> dict[str, Any]:
headers = dict(_DEFAULT_TOKEN_HEADERS)
self._apply_client_auth(data, headers)
response = await self._client.post(url, data=data, headers=headers)
if response.status_code >= 500:
response.raise_for_status()
token: dict[str, Any] = response.json()
if "error" in token:
raise OAuthError(
error=token["error"], description=token.get("error_description")
)
# Mirror authlib's OAuth2Token: derive expires_at from expires_in so
# the stored raw token data keeps the same shape as before.
if token.get("expires_at") is not None:
try:
token["expires_at"] = int(token["expires_at"])
except ValueError:
if token.get("expires_in"):
token["expires_at"] = int(time.time()) + int(token["expires_in"])
elif token.get("expires_in"):
token["expires_at"] = int(time.time()) + int(token["expires_in"])
return token
async def fetch_token(
self,View on GitHub (pinned to 1f02114297)
Solutions
- Read the wrapped error/description to identify the provider's specific code (e.g. invalid_grant => re-authenticate the user)
- If invalid_grant on refresh, clear the stored tokens and redirect the user through the authorization flow again
- Verify client_id, client_secret, and redirect_uri exactly match the provider app configuration
- Check provider status/logs — some errors indicate provider-side revocation or outage
Example fix
// before: blindly reusing a rotated refresh token
await client.refresh_token(refresh_token=old_token)
// after: handle invalid_grant by full re-auth
try:
await client.refresh_token(refresh_token=old_token)
except OAuthError as e:
if e.error == "invalid_grant":
start_authorization_flow(user) Defensive patterns
Strategy: try-catch
Validate before calling
# Pre-flight: verify provider credentials before serving traffic
resp = await httpx_client.get(f"{issuer}/.well-known/openid-configuration")
resp.raise_for_status() Try / catch
try:
token = await client.refresh_token(refresh_token=rt)
except OAuthError as e:
if e.error in {"invalid_grant", "invalid_token"}:
clear_stored_tokens(user)
return redirect_to_authorization(user)
raise # invalid_client etc: config problem, do not retry Prevention
- Treat invalid_grant as 're-authenticate', not 'retry'
- Keep client_id/secret/redirect_uri in sync with provider app settings
- Refresh tokens proactively before expiry; assume refresh tokens may rotate
When it happens
Trigger: Any token endpoint call — authorization-code exchange (fetch_token) or refresh_token — where the provider returns 200/4xx with {"error": ...}: expired/revoked refresh tokens, mismatched redirect_uri, wrong client credentials, or invalid authorization codes.
Common situations: Refresh token rotated/expired by the provider; client secret changed in the provider dashboard; redirect URI mismatch between config and provider settings; clock skew invalidating codes.
Related errors
- Unexpected authorization response: {response.status_code}
- {str(e) from SSRFError/SSRFFetchError}
- str(e)
- User server did not start on port {mcp_port}
- The device authorization request expired
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/f463e6e4c8f4cbc7.
Report an issue: GitHub.