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

  1. Read the wrapped error/description to identify the provider's specific code (e.g. invalid_grant => re-authenticate the user)
  2. If invalid_grant on refresh, clear the stored tokens and redirect the user through the authorization flow again
  3. Verify client_id, client_secret, and redirect_uri exactly match the provider app configuration
  4. 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

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


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