sipeed/picoclaw · error

Failed to save config: %v

Error message

Failed to save config: %v

What it means

HTTP 500 returned by POST /api/models (handleAddModel) when config.SaveConfig(h.configPath, cfg) fails while persisting the appended entry. SaveConfig first writes the companion .security.yml (failure returns immediately), then marshals and atomically writes config.json via fileutil.WriteFileAtomic. Typical causes: read-only or wrong-owner config dir, disk full, or a concurrent writer racing the atomic replace.

Source

Thrown at web/backend/api/models.go:348

		http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest)
		return
	}

	if mc.APIKey != "" {
		mc.ModelConfig.SetAPIKey(mc.APIKey)
	}

	cfg, err := config.LoadConfig(h.configPath)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
		return
	}

	cfg.ModelList = append(cfg.ModelList, &mc.ModelConfig)
	normalizeStoredModelProviders(cfg)

	if err := config.SaveConfig(h.configPath, cfg); err != nil {
		http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"status": "ok",
		"index":  len(cfg.ModelList) - 1,
	})
}

// handleUpdateModel replaces a model configuration entry at the given index.
// If the request body omits api_key (or sends an empty string), the existing
// stored key is preserved so callers can update only api_base / proxy without
// exposing or clearing the secret.
//
//	PUT /api/models/{index}
func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
	idx, err := strconv.Atoi(r.PathValue("index"))

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check the backend log — SaveConfig logs details of the .security.yml or WriteFileAtomic failure
  2. Ensure the config directory is writable by the service user: chmod u+w <config dir>, chown appropriately
  3. Free disk space / raise quota if ENOSPC
  4. Close competing config writers (other tabs, CLI sessions) and retry the POST once

Example fix

# before: backend user cannot write the config dir
$ ls -ld ~/.config/myapp
drwxr-xr-x 2 root root 4096 ... /home/user/.config/myapp

# after: give the running user ownership
$ sudo chown -R $(whoami) ~/.config/myapp
Defensive patterns

Strategy: try-catch

Validate before calling

// before adding, confirm the config service is writable via a health probe
// e.g. a HEAD/OPTIONS on /api/models plus one no-op update in staging

Try / catch

const res = await fetch('/api/models', { method: 'POST', body });
if (res.status === 500 && (await res.text()).includes('Failed to save config')) {
  // do NOT resend blindly: report the cause (perms/disk/concurrent write) to the operator
}

Prevention

When it happens

Trigger: POST /api/models succeeds in memory but the write fails: ENOSPC on a full disk, EACCES on ~/.config/<app> not writable by the service user, a second process holding/locking the file, or saveSecurityConfig failing to write .security.yml.

Common situations: Running the backend under a systemd/nginx user that lacks write access to the config dir; small root partitions or quotas; two UI tabs (or CLI + web UI) saving config at the same moment; antivirus/backup tools briefly locking the config path on some platforms.

Related errors


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