siyuan-note/siyuan · error
OAuth token endpoint returned no access token
Error message
OAuth token endpoint returned no access token
What it means
The token endpoint returned a 2xx response, but the JSON body's `access_token` field was empty. SiYuan refuses to treat a token response as valid when the access token itself is missing, since the subsequent API call would have no credential to send.
Source
Thrown at kernel/mcp/client/oauth.go:687
}
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 {
case "client_secret_basic":
if req != nil {
req.SetBasicAuth(url.QueryEscape(credential.ClientID), url.QueryEscape(credential.ClientSecret))
}
default:
if values != nil {
values.Set("client_id", credential.ClientID)
if credential.TokenAuthMethod == "client_secret_post" && credential.ClientSecret != "" {
values.Set("client_secret", credential.ClientSecret)View on GitHub (pinned to 251596fc0d)
Solutions
- Capture and print the raw token-endpoint response body to confirm it is a valid OAuth token JSON with an `access_token` field.
- Verify the endpoint URL is the token endpoint, not a userinfo or introspection endpoint.
- Re-check the `grant_type` and parameters sent so the IdP actually issues an access token.
Defensive patterns
Strategy: validation
Validate before calling
// Decode the token response defensively and require access_token before use.
var tok oauthTokenResponse
if err := json.Unmarshal(body, &tok); err != nil {
return fmt.Errorf("token response not JSON: %w", err)
}
if strings.TrimSpace(tok.AccessToken) == "" {
return fmt.Errorf("no access_token in body: %s", string(body))
} Prevention
- Point the token endpoint at the real OAuth token URL, not userinfo/introspection.
- Inspect the raw body when integration fails to confirm the expected fields are present.
- Ensure grant_type and parameters actually cause the IdP to issue an access token.
When it happens
Trigger: `oauthTokenResponse.AccessToken` is empty after a successful `json.Unmarshal`. The IdP returned 200 with a body that omits `access_token` (e.g. it returned only an `id_token`, an error wrapped in 200, or a non-OAuth JSON shape).
Common situations: The server is not actually an OAuth 2.0 token endpoint (pointed at a userinfo or userinfo-like endpoint). The grant returned an error that the IdP wrapped with a 200 status. The `Accept` header negotiation returned a non-token content type whose JSON lacks the field.
Related errors
- OAuth issuer mismatch
- OAuth token endpoint returned %s
- mcp oauth authorization required
- parse OAuth challenge: %w
- server returned %s without an OAuth Bearer challenge
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/c0d68be921a16b77.
Report an issue: GitHub.