sipeed/picoclaw · error

failed to update config: %v

Error message

failed to update config: %v

What it means

Returned as HTTP 500 by POST /api/oauth/logout when the credential was deleted but syncProviderAuthMethod(provider, "") failed while rewriting config.json (LoadConfig or SaveConfig on h.configPath). Important state detail: by the time this fires the credential is already gone from the store, so the system is left half-logged-out — no credential, but model entries in config still carrying the old auth_method. A retry of logout will re-delete (harmlessly) and retry the config sync.

Source

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

		Provider string `json:"provider"`
	}
	if err = json.Unmarshal(body, &req); err != nil {
		http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
		return
	}

	provider, err := normalizeOAuthProvider(req.Provider)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if err := oauthDeleteCredential(provider); err != nil {
		http.Error(w, fmt.Sprintf("failed to delete credential: %v", err), http.StatusInternalServerError)
		return
	}
	if err := h.syncProviderAuthMethod(provider, ""); err != nil {
		http.Error(w, fmt.Sprintf("failed to update config: %v", err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"status":   "ok",
		"provider": provider,
	})
}

func renderOAuthCallbackPage(w http.ResponseWriter, flowID, status, title, errMsg string) {
	payload := map[string]string{
		"type":   "picoclaw-oauth-result",
		"flowId": flowID,
		"status": status,
	}
	if errMsg != "" {
		payload["error"] = errMsg

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check config.json at the backend's configured path (h.configPath) parses as valid JSON — LoadConfig failing on a corrupt file is the most common cause.
  2. Make the file and directory writable by the backend process user (chown/chmod) or remount writable.
  3. Retry POST /api/oauth/logout after fixing the file; the credential delete is idempotent and the retry completes the auth_method cleanup.
  4. If retry is impossible, manually clear auth_method on the provider's model entries in config.json to finish the logout.

Example fix

# before
$ curl -X POST localhost:8080/api/oauth/logout -d '{"provider":"openai"}'
{"message":"failed to update config: ... permission denied"}  # credential deleted, config not updated

# after
$ sudo chown $(id -u):$(id -g) ~/.config/picoclaw/config.json
$ curl -X POST localhost:8080/api/oauth/logout -H 'Content-Type: application/json' -d '{"provider":"openai"}'
{"status":"ok","provider":"openai"}
Defensive patterns

Strategy: try-catch

Validate before calling

JSON.parse(await fs.readFile(configPath, 'utf8')); // config.json must parse before logout
await fs.access(configPath, fs.constants.W_OK);

Try / catch

const res = await fetch('/api/oauth/logout', {...});
if (res.status === 500) {
  const { message } = await res.json();
  if (/update config/i.test(message ?? '')) {
    // credential already deleted — retry after config becomes writable; retry is idempotent
    await fixConfigPermissions();
    return fetch('/api/oauth/logout', {...});
  }
  throw new Error(message);
}

Prevention

When it happens

Trigger: POST /api/oauth/logout where config.json is unreadable (corrupt JSON -> LoadConfig error), not writable by the backend user, on a read-only mount, or the disk is full (SaveConfig).

Common situations: config.json owned by root after an install step; read-only config mount in Docker; user edited config.json and broke the JSON; disk full. Retry loops that assume logout is atomic can strand the half-state.

Related errors


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