sipeed/picoclaw · error

Failed to save launcher config: %v

Error message

Failed to save launcher config: %v

What it means

Returned by PUT /api/system/launcher-config when launcherconfig.Save (launcherconfig/config.go:145-161) fails to persist the (already validated) config. Failure points: MkdirAll on the config directory, MarshalIndent (practically never for this struct), or os.WriteFile with mode 0600 - permission denied, disk full, or a read-only filesystem. Save re-validates first, but since the handler validated immediately before, an error here is almost always filesystem I/O.

Source

Thrown at web/backend/api/launcher_config.go:97

	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to load launcher config: %v", err), http.StatusInternalServerError)
		return
	}
	cfg.Port = payload.Port
	cfg.Public = payload.Public
	cfg.AllowedCIDRs = append([]string(nil), payload.AllowedCIDRs...)
	if payload.AllowLocalhostBypass != nil {
		cfg.AllowLocalhostBypass = *payload.AllowLocalhostBypass
	}
	cfg.TrustedProxyCIDRs = append([]string(nil), payload.TrustedProxyCIDRs...)
	cfg.LegacyLauncherToken = ""
	if err := launcherconfig.Validate(cfg); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

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

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(launcherConfigPayload{
		Port:                 cfg.Port,
		Public:               cfg.Public,
		AllowedCIDRs:         append([]string(nil), cfg.AllowedCIDRs...),
		AllowLocalhostBypass: cfg.AllowLocalhostBypass,
		TrustedProxyCIDRs:    append([]string(nil), cfg.TrustedProxyCIDRs...),
	})
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check the wrapped error - 'failed to create directory' vs a plain write error points at different paths
  2. Free disk space if the volume is full
  3. Make the config directory writable by the backend user (ownership plus mode), and confirm the mount is not read-only
  4. Retry the PUT after fixing the filesystem - the failed write left the previous config intact
Defensive patterns

Strategy: retry

Try / catch

for (let attempt = 0; attempt < 2; attempt++) {
  const res = await fetch('/api/system/launcher-config', {method: 'PUT', ...});
  if (res.ok) return res.json();
  const text = await res.text();
  if (text.startsWith('Failed to save launcher config')) {
    if (text.includes('read-only') || text.includes('permission denied')) throw new Error('config dir not writable by backend - fix ownership/mount'); // not retryable
    await sleep(1000);   // transient (disk pressure): safe to retry, previous file untouched
    continue;
  }
  throw new Error(text);
}

Prevention

When it happens

Trigger: Config directory on a read-only mount (container overlay, k8s ConfigMount); disk full; launcher-config.json owned by another user after a migration; SELinux/AppArmor denying writes to the path.

Common situations: Containers started with a read-only volume for config; disk exhaustion on small edge devices; files created by root during setup then served by an unprivileged service.

Related errors


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