sipeed/picoclaw · error

failed to load credentials: %v

Error message

failed to load credentials: %v

What it means

Returned by GET /api/oauth/providers (handleListOAuthProviders) when oauthGetCredential → auth.GetCredential(provider) errors while loading the credential store (a JSON auth file on disk, written atomically with 0600 perms). Important distinction: a provider that is simply not logged in returns a nil credential with no error; this 500 means the store file itself could not be read or parsed (%v has the cause).

Source

Thrown at web/backend/api/oauth.go:135

}

// registerOAuthRoutes binds OAuth login/logout endpoints to the ServeMux.
func (h *Handler) registerOAuthRoutes(mux *http.ServeMux) {
	mux.HandleFunc("GET /api/oauth/providers", h.handleListOAuthProviders)
	mux.HandleFunc("POST /api/oauth/login", h.handleOAuthLogin)
	mux.HandleFunc("GET /api/oauth/flows/{id}", h.handleGetOAuthFlow)
	mux.HandleFunc("POST /api/oauth/flows/{id}/poll", h.handlePollOAuthFlow)
	mux.HandleFunc("POST /api/oauth/logout", h.handleOAuthLogout)
	mux.HandleFunc("GET /oauth/callback", h.handleOAuthCallback)
}

func (h *Handler) handleListOAuthProviders(w http.ResponseWriter, r *http.Request) {
	providersResp := make([]oauthProviderStatus, 0, len(oauthProviderOrder))

	for _, provider := range oauthProviderOrder {
		cred, err := oauthGetCredential(provider)
		if err != nil {
			http.Error(w, fmt.Sprintf("failed to load credentials: %v", err), http.StatusInternalServerError)
			return
		}

		item := oauthProviderStatus{
			Provider:    provider,
			DisplayName: oauthProviderLabels[provider],
			Methods:     oauthProviderMethods[provider],
			Status:      "not_logged_in",
		}
		if cred != nil {
			item.LoggedIn = true
			item.AuthMethod = cred.AuthMethod
			item.AccountID = cred.AccountID
			item.Email = cred.Email
			item.ProjectID = cred.ProjectID
			if !cred.ExpiresAt.IsZero() {
				item.ExpiresAt = cred.ExpiresAt.Format(time.RFC3339)
			}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Read %v: 'permission denied' → chown/chmod the auth file (0600) and its directory to the backend user
  2. 'unexpected end of JSON input' → the store is truncated; rename the corrupt file aside, restart, and re-login providers
  3. Verify the auth file path the backend uses (authFilePath()) exists in the environment the service actually runs in

Example fix

# before
$ ls -l ~/.picoclaw/auth.json
-rw------- 1 root root 468
(backend runs as 'beagle')

# after
$ chown beagle:beagle ~/.picoclaw/auth.json && chmod 600 ~/.picoclaw/auth.json
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const res = await fetch('/api/oauth/providers');
  if (res.status === 500 && (await res.text()).includes('load credentials')) {
    /* credential store unreadable — surface to admin (permissions/corrupt auth file), do not retry */
  }
} catch (e) { /* transport */ }

Prevention

When it happens

Trigger: GET /api/oauth/providers while the auth credentials file is missing-but-unreadable, has wrong permissions, or is corrupted JSON. Any single provider's store load failure aborts the whole listing — the loop returns on first error.

Common situations: Auth file created by root, then backend restarted as another user (permission denied); partial write from a crash left truncated JSON; file locked or SELinux-denied on hardened systems; auth dir moved/deleted.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/6c3a74ea0e1c91b6. Report an issue: GitHub.