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
- Read %v: 'permission denied' → chown/chmod the auth file (0600) and its directory to the backend user
- 'unexpected end of JSON input' → the store is truncated; rename the corrupt file aside, restart, and re-login providers
- 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
- Keep the auth credentials file owned by the backend user with 0600 perms
- Never hand-edit the auth store; use the login/logout endpoints
- Back up the auth file before user/service changes; alert on any 500 from this read-only endpoint
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
- credential: failed to read credential file %q: %w
- credential: cannot read SSH key %q: %w
- token login failed: %v
- failed to delete credential: %v
- create output dir: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/6c3a74ea0e1c91b6.
Report an issue: GitHub.