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

  1. Inspect the full `resp.Status` value embedded in the error and the IdP's logs to find the precise rejection reason.
  2. Verify `client_id`/`client_secret`, `redirect_uri`, and `scope` match the IdP's client registration exactly.
  3. Confirm `TokenAuthMethod` matches the IdP's registered token-endpoint auth method.
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/bd22070949aeedfc. Report an issue: GitHub.