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
- Run JSON.parse on the editor content locally before saving — if it throws, fix the syntax first
- Read the PUT /api/config response body in the network tab; the backend usually names the offending key
- Diff your raw text against the loaded config to spot accidental deletions of required sections
- 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
- Parse and schema-check the raw text client-side before enabling the save button
- Keep a diff view against the last saved config so accidental deletions are visible
- Paste JSON, never TOML/YAML, into the raw editor; comments and trailing commas are invalid JSON
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
- Failed to load config
- Failed to fetch config
- ${label} must be a JSON object.
- ${label}.${key} must be a string.
- Request failed with status ${res.status}
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/0b8f3fc449c77035.
Report an issue: GitHub.