siyuan-note/siyuan · error

OAuth token endpoint returned

Error message

OAuth token endpoint returned %s

What it means

oauthTokenRequest POSTs to the token endpoint and expects either a 2xx JSON token response or an RFC 6749 JSON error body with a non-empty 'error' code. When the status is non-2xx AND the body is not a parseable OAuth error (or lacks the error code), it fails with this generic message including the HTTP status text, because the server's actual rejection reason is unknown.

Solutions

  1. Verify credential.TokenEndpoint matches the token_endpoint advertised in the authorization server metadata.
  2. Capture the raw response body (curl the token endpoint) to see the real error page/message.
  3. Check for WAF/proxy interference (Cloudflare challenges, SSO login HTML) between the client and IdP.
  4. Confirm TokenAuthMethod (none/client_secret_post/client_secret_basic) is one the IdP accepts; a wrong method can yield opaque 401s.
  5. Retry during IdP outages; 5xx is usually transient.

Example fix

// before: stale endpoint after IdP migration
token_endpoint: "https://old-idp.example.com/oauth/token"
// after: use current metadata value
token_endpoint: "https://auth.example.com/oauth/token"
Defensive patterns

Strategy: try-catch

Validate before calling

resp, err := http.PostForm(tokenEndpoint, url.Values{"grant_type": {"refresh_token"}, "refresh_token": {tok}})
if err != nil || resp.StatusCode < 200 || resp.StatusCode >= 300 {
    // probe the token endpoint manually to see the raw error before refreshing
}

Try / catch

cred, permanent, err := refreshOAuthCredential(ctx, client, credential)
if err != nil {
    if permanent {
        startFreshAuthorizeFlow(server) // invalid_grant/invalid_client
    } else {
        inspectTokenEndpointRawResponse() // non-protocol error, check body/status
    }
}

Prevention

When it happens

Trigger: Called from refreshOAuthCredential (grant_type=refresh_token) when the token endpoint returns e.g. 500, 403, 404, or an HTML/empty error body with a non-2xx status — anything outside 200-299 that cannot be decoded into an oauthTokenError with a code.

Common situations: Token endpoint URL wrong (404 from a router); IdP returns HTML error page (WAF/Cloudflare challenge); 500 during IdP outage; auth method rejected with a non-protocol 401 body; rate limiting returning plain text.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/bd22070949aeedfc. Report an issue: GitHub.

Appendix: 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 9f775e8a12)