{"record":{"id":"92b4a2aefeaed249","repo":"sipeed/picoclaw","slug":"failed-to-update-config-v","errorCode":null,"errorMessage":"failed to update config: %v","messagePattern":"failed to update config: (.+?)","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"web/backend/api/oauth.go","lineNumber":482,"sourceCode":"\t\tProvider string `json:\"provider\"`\n\t}\n\tif err = json.Unmarshal(body, &req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tprovider, err := normalizeOAuthProvider(req.Provider)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif err := oauthDeleteCredential(provider); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"failed to delete credential: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tif err := h.syncProviderAuthMethod(provider, \"\"); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"failed to update config: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(map[string]any{\n\t\t\"status\":   \"ok\",\n\t\t\"provider\": provider,\n\t})\n}\n\nfunc renderOAuthCallbackPage(w http.ResponseWriter, flowID, status, title, errMsg string) {\n\tpayload := map[string]string{\n\t\t\"type\":   \"picoclaw-oauth-result\",\n\t\t\"flowId\": flowID,\n\t\t\"status\": status,\n\t}\n\tif errMsg != \"\" {\n\t\tpayload[\"error\"] = errMsg","sourceCodeStart":464,"sourceCodeEnd":500,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/oauth.go#L464-L500","documentation":"Returned as HTTP 500 by POST /api/oauth/logout when the credential was deleted but syncProviderAuthMethod(provider, \"\") failed while rewriting config.json (LoadConfig or SaveConfig on h.configPath). Important state detail: by the time this fires the credential is already gone from the store, so the system is left half-logged-out — no credential, but model entries in config still carrying the old auth_method. A retry of logout will re-delete (harmlessly) and retry the config sync.","triggerScenarios":"POST /api/oauth/logout where config.json is unreadable (corrupt JSON -> LoadConfig error), not writable by the backend user, on a read-only mount, or the disk is full (SaveConfig).","commonSituations":"config.json owned by root after an install step; read-only config mount in Docker; user edited config.json and broke the JSON; disk full. Retry loops that assume logout is atomic can strand the half-state.","solutions":["Check config.json at the backend's configured path (h.configPath) parses as valid JSON — LoadConfig failing on a corrupt file is the most common cause.","Make the file and directory writable by the backend process user (chown/chmod) or remount writable.","Retry POST /api/oauth/logout after fixing the file; the credential delete is idempotent and the retry completes the auth_method cleanup.","If retry is impossible, manually clear auth_method on the provider's model entries in config.json to finish the logout."],"exampleFix":"# before\n$ curl -X POST localhost:8080/api/oauth/logout -d '{\"provider\":\"openai\"}'\n{\"message\":\"failed to update config: ... permission denied\"}  # credential deleted, config not updated\n\n# after\n$ sudo chown $(id -u):$(id -g) ~/.config/picoclaw/config.json\n$ curl -X POST localhost:8080/api/oauth/logout -H 'Content-Type: application/json' -d '{\"provider\":\"openai\"}'\n{\"status\":\"ok\",\"provider\":\"openai\"}","handlingStrategy":"try-catch","validationCode":"JSON.parse(await fs.readFile(configPath, 'utf8')); // config.json must parse before logout\nawait fs.access(configPath, fs.constants.W_OK);","typeGuard":null,"tryCatchPattern":"const res = await fetch('/api/oauth/logout', {...});\nif (res.status === 500) {\n  const { message } = await res.json();\n  if (/update config/i.test(message ?? '')) {\n    // credential already deleted — retry after config becomes writable; retry is idempotent\n    await fixConfigPermissions();\n    return fetch('/api/oauth/logout', {...});\n  }\n  throw new Error(message);\n}","preventionTips":["Ensure config.json is readable/writable by the backend before calling logout.","Treat this error as a half-completed logout: always retry once the config issue is fixed so auth_method entries are cleared.","Alert on it — a config.json that cannot be saved will break every settings change too."],"tags":["oauth","http-500","config","filesystem","logout","partial-state"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}