{"record":{"id":"38a4a1e5d8e713cd","repo":"sipeed/picoclaw","slug":"failed-to-load-config","errorCode":null,"errorMessage":"Failed to load config","messagePattern":"Failed to load config","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/frontend/src/components/config/config-page.tsx","lineNumber":126,"sourceCode":"    useState<LauncherForm>(EMPTY_LAUNCHER_FORM)\n  const [launcherBaseline, setLauncherBaseline] =\n    useState<LauncherForm>(EMPTY_LAUNCHER_FORM)\n  const [autoStartEnabled, setAutoStartEnabled] = useState(false)\n  const [autoStartBaseline, setAutoStartBaseline] = useState(false)\n  const [saving, setSaving] = useState(false)\n  const [showFactoryResetDialog, setShowFactoryResetDialog] = useState(false)\n\n  const { data, isLoading, error } = useQuery({\n    queryKey: [\"config\"],\n    queryFn: async () => {\n      const controller = new AbortController()\n      const timer = setTimeout(() => controller.abort(), 5000)\n      try {\n        const res = await launcherFetch(\"/api/config\", {\n          signal: controller.signal,\n        })\n        if (!res.ok) {\n          throw new Error(\"Failed to load config\")\n        }\n        return res.json()\n      } finally {\n        clearTimeout(timer)\n      }\n    },\n  })\n\n  const { data: launcherConfig, isLoading: isLauncherLoading } = useQuery({\n    queryKey: [\"system\", \"launcher-config\"],\n    queryFn: getLauncherConfig,\n  })\n\n  const { data: versionInfo } = useQuery({\n    queryKey: [\"system\", \"version\"],\n    queryFn: getSystemVersionInfo,\n    staleTime: 5 * 60 * 1000,\n  })","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/frontend/src/components/config/config-page.tsx#L108-L144","documentation":"Thrown in the react-query queryFn of ConfigPage (web/frontend/src/components/config/config-page.tsx:126) when GET /api/config through launcherFetch resolves with a non-2xx status. launcherFetch sends same-origin credentials and redirects to /launcher-login on 401 JSON responses, so this message surfaces for other statuses (500, 404, 403) or a 401 received while already on an auth page. A separate 5-second AbortController timer races the fetch; an abort rejects with AbortError before this throw, so this error strictly means 'server answered with an error status'.","triggerScenarios":"Launcher backend returned 500 for /api/config (config file unreadable/corrupt), 404 when the frontend is served from a different base path, 403 from an auth-protected setup, or 401 while the page is the login/setup page itself (redirect suppressed by isLauncherAuthPath).","commonSituations":"Config file on disk has bad permissions or invalid TOML/JSON so the backend fails to serialize it; frontend deployed behind a reverse proxy that routes /api elsewhere; session cookie expired exactly while on the login page; backend restarting and returning 502/503 from the proxy.","solutions":["Open the network tab and read the actual status and body of GET /api/config — the fix depends on the status code","For 500: check launcher backend logs and validate the config file it loads (permissions, syntax)","For 404/502 behind a proxy: fix proxy routing so /api/config reaches the launcher backend","For 401: log in at /launcher-login, or retry after the backend finishes restarting"],"exampleFix":"// before\nif (!res.ok) {\n  throw new Error(\"Failed to load config\")\n}\n\n// after\nif (!res.ok) {\n  const body = await res.text().catch(() => \"\")\n  throw new Error(`Failed to load config (HTTP ${res.status})${body ? \": \" + body : \"\"}`)\n}","handlingStrategy":"retry","validationCode":"async function pingConfig(): Promise<boolean> {\n  try {\n    const res = await launcherFetch(\"/api/config\", { method: \"HEAD\" })\n    return res.ok\n  } catch {\n    return false\n  }\n}","typeGuard":null,"tryCatchPattern":"// react-query: let the query error state carry it; distinguish timeout from HTTP error\ntry {\n  const res = await launcherFetch(\"/api/config\", { signal: controller.signal })\n  if (!res.ok) throw new Error(`Failed to load config (HTTP ${res.status})`)\n} catch (err) {\n  if (err instanceof DOMException && err.name === \"AbortError\") {\n    // 5s timeout — treat as transient, allow retry\n  }\n  throw err\n}","preventionTips":["Keep the backend health-checked and surface its status before the config page loads","Include the HTTP status in the thrown message so users can self-diagnose 500 vs 401 vs 404","Retry transient 502/503 once; never retry 4xx blindly","Ensure reverse-proxy /api routes are tested in deployment smoke checks"],"tags":["network","http","frontend","react-query","config","api"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}