sipeed/picoclaw · error

Failed to save config

Error message

Failed to save config

What it means

Thrown in the useMutation mutationFn of RawConfigPage (web/frontend/src/components/config/raw-config-page.tsx:50) when PUT /api/config with the editor's raw text body resolves with a non-2xx status. Unlike the form-based page, this endpoint receives the text as-is, so the backend rejects syntactically invalid JSON or schema-violating configs with a 400; auth and lock conditions can also fail it.

Source

Thrown at web/frontend/src/components/config/raw-config-page.tsx:50

    queryKey: ["config"],
    queryFn: async () => {
      const res = await launcherFetch("/api/config")
      if (!res.ok) {
        throw new Error("Failed to fetch config")
      }
      return res.json()
    },
  })

  const mutation = useMutation({
    mutationFn: async (newConfig: string) => {
      const res = await launcherFetch("/api/config", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: newConfig,
      })
      if (!res.ok) {
        throw new Error("Failed to save config")
      }
    },
    onSuccess: (_, submittedConfig) => {
      try {
        const savedConfig = JSON.parse(submittedConfig)
        setLastSavedConfig(savedConfig)
        setIsDirty(false)
        queryClient.invalidateQueries({ queryKey: ["config"] })
      } catch {
        queryClient.invalidateQueries({ queryKey: ["config"] })
      }
      void refreshGatewayState({ force: true }).then((gateway) => {
        showSaveSuccessOrRestartToast(
          t,
          t("pages.config.save_success"),
          t("navigation.config"),
          gateway?.restartRequired === true,
        )

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Run JSON.parse on the editor content locally before saving — if it throws, fix the syntax first
  2. Read the PUT /api/config response body in the network tab; the backend usually names the offending key
  3. Diff your raw text against the loaded config to spot accidental deletions of required sections
  4. If 401, re-authenticate at /launcher-login and retry

Example fix

// before
const res = await launcherFetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: newConfig })
if (!res.ok) throw new Error("Failed to save config")

// after
const res = await launcherFetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: newConfig })
if (!res.ok) {
  const detail = await res.text().catch(() => "")
  throw new Error(`Failed to save config (HTTP ${res.status})${detail ? ": " + detail : ""}`)
}
Defensive patterns

Strategy: validation

Validate before calling

try {
  JSON.parse(newConfig)
} catch (e) {
  setEditorError(`Invalid JSON: ${(e as SyntaxError).message}`)
  return // do not PUT
}

Type guard

function isParsableJSON(text: string): boolean {
  try {
    JSON.parse(text)
    return true
  } catch {
    return false
  }
}

Try / catch

mutation = useMutation({
  mutationFn: async (newConfig: string) => {
    const res = await launcherFetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: newConfig })
    if (!res.ok) {
      const detail = await res.text().catch(() => "")
      throw new Error(`Failed to save config (HTTP ${res.status})${detail ? ": " + detail : ""}`)
    }
  },
})

Prevention

When it happens

Trigger: Clicking save in the raw editor with invalid JSON (trailing commas, comments, unbalanced braces), a structurally valid but schema-invalid config (unknown keys, wrong value types), an expired session (401 on auth page), or a locked/readonly config file (500).

Common situations: Hand-editing the raw config and introducing a syntax error; copying a config from an incompatible version; pasting TOML into the JSON editor; editing while the file's permissions changed under you.

Related errors


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