siyuan-note/siyuan · error
OAuth token endpoint returned %s
Error message
OAuth token endpoint returned %s
What it means
The OAuth token endpoint responded with a non-2xx HTTP status, and either the response body did not parse as an `oauthTokenError` JSON object or the parsed object had no `code` field. Because no structured error was available, SiYuan surfaces the raw HTTP status line (e.g. `400 Bad Request`).
Source
Thrown at kernel/mcp/client/oauth.go:678
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
applyOAuthClientAuthentication(nil, req, credential)
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
tokenErr := &oauthTokenError{}
if json.Unmarshal(body, tokenErr) != nil || tokenErr.Code == "" {
return nil, nil, fmt.Errorf("OAuth token endpoint returned %s", resp.Status)
}
return nil, tokenErr, tokenErr
}
result := &oauthTokenResponse{}
if err = json.Unmarshal(body, result); err != nil {
return nil, nil, err
}
if result.AccessToken == "" {
return nil, nil, fmt.Errorf("OAuth token endpoint returned no access token")
}
if result.TokenType != "" && !strings.EqualFold(result.TokenType, "Bearer") {
return nil, nil, fmt.Errorf("OAuth token endpoint returned unsupported token type %q", result.TokenType)
}
return result, nil, nil
}
func applyOAuthClientAuthentication(values url.Values, req *http.Request, credential oauthCredential) {
switch credential.TokenAuthMethod {View on GitHub (pinned to 251596fc0d)
Solutions
- Inspect the full `resp.Status` value embedded in the error and the IdP's logs to find the precise rejection reason.
- Verify `client_id`/`client_secret`, `redirect_uri`, and `scope` match the IdP's client registration exactly.
- Confirm `TokenAuthMethod` matches the IdP's registered token-endpoint auth method.
- If the body is JSON, the IdP may use a non-standard error shape; capture the raw body to decode it manually.
Defensive patterns
Strategy: retry
Validate before calling
// Inspect resp.Status; retry only transient (5xx/network) failures, not 4xx config errors.
status := resp.Status
if strings.HasPrefix(status, "5") || isNetworkErr(err) {
backoffRetry(ctx, exchange)
} else {
logAndSurface(err) // 4xx: fix client config, do not retry
} Try / catch
// Retry only transient token-endpoint failures; surface 4xx as config errors.
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
_, tokenErr, err := oauthTokenRequest(ctx, client, cred, values)
if err == nil {
break
}
if tokenErr != nil && (tokenErr.Code == "invalid_grant" || tokenErr.Code == "invalid_client") {
return err // permanent; do not retry
}
lastErr = err
time.Sleep(backoff(attempt))
}
return lastErr Prevention
- Keep `client_id`/`client_secret`, `redirect_uri`, and `scope` aligned with the IdP registration.
- Match `TokenAuthMethod` to the IdP's configured token auth style.
- Capture the raw response body when debugging to decode non-standard IdP error shapes.
When it happens
Trigger: `oauthTokenRequest` POSTs to `credential.TokenEndpoint` and receives `statusCode < 200 || >= 300`. Occurs during initial token exchange or refresh-token rotation when the IdP rejects the request.
Common situations: Expired/already-used authorization code, wrong `client_secret`, mismatched `redirect_uri`, requested scopes the client is not allowed, or a transient IdP 5xx. A `TokenAuthMethod` (`client_secret_basic` vs `client_secret_post`) configured differently from what the IdP expects produces a 401.
Related errors
- OAuth revocation endpoint returned %s
- discover OAuth authorization server: %w
- register OAuth client: %w
- OAuth protected resource metadata not found
- OAuth issuer mismatch
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/bd22070949aeedfc.
Report an issue: GitHub.