siyuan-note/siyuan · error

refresh OAuth credentials

Error message

refresh OAuth credentials: %w

What it means

The stored credential has a refresh token and the code attempted refreshOAuthCredential, but the refresh failed with a non-permanent (retryable) error — e.g. network failure, 5xx, or timeout at the token endpoint. Because the error is not permanent, the library keeps the stored tokens and surfaces the failure instead of wiping credentials.

Solutions

  1. Retry Authorize after confirming the token endpoint is reachable (curl the token URL)
  2. Check the wrapped refreshErr for the underlying cause (network vs HTTP status) and fix that
  3. If the IdP rejected the grant permanently (invalid_grant), clear the stored MCP OAuth credential for this server so a fresh authorization flow runs
  4. Verify system clock correctness if the underlying error mentions token validity
Defensive patterns

Strategy: retry

Try / catch

if err := h.Authorize(ctx, req, resp); err != nil && strings.Contains(err.Error(), "refresh OAuth credentials:") {
    // transient by design; schedule a retry with backoff
    time.AfterFunc(backoff, retryAuthorize)
}

Prevention

When it happens

Trigger: Authorize is called with a challenge error other than insufficient_scope, a valid non-expired registration, and a stored RefreshToken; the token endpoint returns a transient error (connection refused, 500, timeout) during the refresh_token grant.

Common situations: IdP briefly down or rate-limiting the token endpoint; network drop between SiYuan and the IdP; token endpoint TLS cert rotation causing transient failures; clock skew causing intermittent validation errors.

Related errors


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

Appendix: source

Thrown at kernel/mcp/client/oauth.go:238

	if hasCredential && credential.Issuer == asm.Issuer {
		credential.TokenEndpoint = asm.TokenEndpoint
		credential.RevocationEndpoint = asm.RevocationEndpoint
	}
	if hasCredential && credential.Issuer == asm.Issuer && credential.RefreshToken != "" &&
		challengeError != "insufficient_scope" && !credential.Rejected && !oauthClientRegistrationExpired(credential) {
		refreshed, permanent, refreshErr := refreshOAuthCredential(ctx, h.client, credential)
		if refreshErr == nil {
			if saveErr := putOAuthCredential(refreshed); saveErr != nil {
				logging.LogWarnf("mcp oauth: save refreshed credentials failed: %s", saveErr)
			}
			h.sourceMu.Lock()
			h.source = &storedOAuthTokenSource{credential: refreshed, client: h.client}
			h.sourceMu.Unlock()
			setMCPRuntimeStateForContext(ctx, h.server.ID, "oauth_retrying", 0, "", "")
			return nil
		}
		if !permanent {
			return fmt.Errorf("refresh OAuth credentials: %w", refreshErr)
		}
		credential.AccessToken = ""
		credential.RefreshToken = ""
		credential.Expiry = time.Time{}
		if saveErr := putOAuthCredential(credential); saveErr != nil {
			logging.LogWarnf("mcp oauth: clear invalid credentials failed: %s", saveErr)
		}
	}
	if !interactive {
		setMCPRuntimeStateForContext(ctx, h.server.ID, "authorization_required", 0, "", "")
		return errOAuthAuthorizationRequired
	}
	if !slices.Contains(asm.CodeChallengeMethodsSupported, "S256") {
		return fmt.Errorf("OAuth authorization server does not support PKCE S256")
	}
	if len(asm.ResponseTypesSupported) > 0 && !slices.Contains(asm.ResponseTypesSupported, "code") {
		return fmt.Errorf("OAuth authorization server does not support the authorization code response type")
	}

View on GitHub (pinned to 9f775e8a12)