{"record":{"id":"0b8f3fc449c77035","repo":"sipeed/picoclaw","slug":"failed-to-save-config","errorCode":null,"errorMessage":"Failed to save config","messagePattern":"Failed to save config","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/frontend/src/components/config/raw-config-page.tsx","lineNumber":50,"sourceCode":"    queryKey: [\"config\"],\n    queryFn: async () => {\n      const res = await launcherFetch(\"/api/config\")\n      if (!res.ok) {\n        throw new Error(\"Failed to fetch config\")\n      }\n      return res.json()\n    },\n  })\n\n  const mutation = useMutation({\n    mutationFn: async (newConfig: string) => {\n      const res = await launcherFetch(\"/api/config\", {\n        method: \"PUT\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: newConfig,\n      })\n      if (!res.ok) {\n        throw new Error(\"Failed to save config\")\n      }\n    },\n    onSuccess: (_, submittedConfig) => {\n      try {\n        const savedConfig = JSON.parse(submittedConfig)\n        setLastSavedConfig(savedConfig)\n        setIsDirty(false)\n        queryClient.invalidateQueries({ queryKey: [\"config\"] })\n      } catch {\n        queryClient.invalidateQueries({ queryKey: [\"config\"] })\n      }\n      void refreshGatewayState({ force: true }).then((gateway) => {\n        showSaveSuccessOrRestartToast(\n          t,\n          t(\"pages.config.save_success\"),\n          t(\"navigation.config\"),\n          gateway?.restartRequired === true,\n        )","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/frontend/src/components/config/raw-config-page.tsx#L32-L68","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nconst res = await launcherFetch(\"/api/config\", { method: \"PUT\", headers: { \"Content-Type\": \"application/json\" }, body: newConfig })\nif (!res.ok) throw new Error(\"Failed to save config\")\n\n// after\nconst res = await launcherFetch(\"/api/config\", { method: \"PUT\", headers: { \"Content-Type\": \"application/json\" }, body: newConfig })\nif (!res.ok) {\n  const detail = await res.text().catch(() => \"\")\n  throw new Error(`Failed to save config (HTTP ${res.status})${detail ? \": \" + detail : \"\"}`)\n}","handlingStrategy":"validation","validationCode":"try {\n  JSON.parse(newConfig)\n} catch (e) {\n  setEditorError(`Invalid JSON: ${(e as SyntaxError).message}`)\n  return // do not PUT\n}","typeGuard":"function isParsableJSON(text: string): boolean {\n  try {\n    JSON.parse(text)\n    return true\n  } catch {\n    return false\n  }\n}","tryCatchPattern":"mutation = useMutation({\n  mutationFn: async (newConfig: string) => {\n    const res = await launcherFetch(\"/api/config\", { method: \"PUT\", headers: { \"Content-Type\": \"application/json\" }, body: newConfig })\n    if (!res.ok) {\n      const detail = await res.text().catch(() => \"\")\n      throw new Error(`Failed to save config (HTTP ${res.status})${detail ? \": \" + detail : \"\"}`)\n    }\n  },\n})","preventionTips":["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"],"tags":["http","config","json","frontend","api","react-query"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}