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 HTTP 2xx with JSON that decoded successfully, but the access_token field was empty or missing. A protocol-compliant token response must contain a non-empty access_token; an empty one makes the result unusable, so oauthTokenRequest rejects it.

Solutions

  1. Verify the token endpoint URL is the actual token_endpoint from the server's metadata, not another API route.
  2. Dump the 200 response body to inspect what the server actually returned.
  3. Confirm the grant is permitted: some IdPs return 200 with an empty token when refresh tokens are revoked — re-authenticate with a fresh authorize flow.
  4. Upgrade or fix the IdP if it violates RFC 6749 by omitting access_token on success.
  5. Check for proxies rewriting the response body.
Defensive patterns

Strategy: validation

Validate before calling

var body map[string]any
json.NewDecoder(resp.Body).Decode(&body)
if at, _ := body["access_token"].(string); at == "" {
    // server returns 200 without access_token; it is non-conformant or the endpoint is wrong
}

Try / catch

if _, _, err := oauthTokenRequest(ctx, client, credential, values); err != nil {
    if strings.Contains(err.Error(), "no access token") {
        log.Error("non-conformant token response; re-authenticating")
        startFreshAuthorizeFlow(server)
    }
}

Prevention

When it happens

Trigger: Called from refreshOAuthCredential when the server responds 200 with a JSON body lacking access_token — e.g. a non-standard success body, a server bug, an HTML 'success' page that coincidentally parses differently, or a response containing only refresh-related fields.

Common situations: Non-conformant IdP implementations; middleware returning 200 with an error-shaped body; grant type silently ignored (server returns empty token instead of an error); misconfigured token endpoint pointing to a non-token API that returns 200 JSON.

Related errors


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

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