{"record":{"id":"7cea1cca77dfd860","repo":"AlexsJones/llmfit","slug":"server-returned-an-invalid-json-response","errorCode":null,"errorMessage":"Server returned an invalid JSON response.","messagePattern":"Server returned an invalid JSON response\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"llmfit-web/src/api.js","lineNumber":110,"sourceCode":"\n  const maxContext = trimOrEmpty(String(filters.maxContext || ''));\n  if (maxContext) {\n    const parsed = Number.parseInt(maxContext, 10);\n    if (Number.isFinite(parsed) && parsed > 0) {\n      params.set('max_context', String(parsed));\n    }\n  }\n\n  appendSimulationParams(params, simulation);\n  return params.toString();\n}\n\nasync function parseJsonOrThrow(response) {\n  let payload;\n  try {\n    payload = await response.json();\n  } catch (err) {\n    throw new Error('Server returned an invalid JSON response.');\n  }\n\n  if (!response.ok) {\n    const message = payload?.error || `Request failed with status ${response.status}.`;\n    throw new Error(message);\n  }\n\n  return payload;\n}\n\nexport async function fetchSystemInfo(simulation = {}, signal) {\n  const query = appendSimulationParams(new URLSearchParams(), simulation).toString();\n  const path = query ? `/api/v1/system?${query}` : '/api/v1/system';\n  const response = await fetch(path, { signal });\n  return parseJsonOrThrow(response);\n}\n\nexport async function fetchModels(filters, simulation = {}, signal) {","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/AlexsJones/llmfit/blob/a9ac7ed91c1729cd93944bc1338e8313daaba8fa/llmfit-web/src/api.js#L92-L128","documentation":"Thrown by parseJsonOrThrow() in the llmfit web dashboard client when response.json() rejects, i.e. the server answered but the body is not parseable JSON. Every /api/v1/* helper (fetchSystemInfo, fetchModels, fetchRuntimes, fetchInstalled, startDownload, fetchDownloadStatus, fetchPlanEstimate) funnels through this function, so the error really means 'the HTTP layer reached something that does not speak JSON' (an HTML error page, an empty body, or a proxy response). Note the original err is discarded, so the underlying parse cause is hidden.","triggerScenarios":"Calling any llmfit-web API helper while the page is served from something other than the llmfit Axum server (Vite dev server without a proxy for /api/v1, a static file server, or the wrong port); a reverse proxy or gateway returning an HTML 502/503/504 page; a truncated or empty response body from a crashed handler; a JSON body with a wrong Content-Type plus malformed content.","commonSituations":"Running `npm run dev` against a backend that is stopped or on a different port; the llmfit server restarted while the dashboard had a poll in flight; corporate proxy/VPN injecting an HTML interstitial; hitting /api/v1/* on a port that serves the built assets but not the API router.","solutions":["Verify the llmfit server (llmfit serve / the Axum process) is running and confirm the port the dashboard targets matches it.","Reproduce with `curl -i http://<host>:<port>/api/v1/system` and inspect the raw body and Content-Type to see what actually answered.","If using the Vite dev server, configure its proxy so /api/v1 forwards to the backend port (or load the dashboard directly from the llmfit server).","If a reverse proxy sits in front, make sure it passes JSON responses through and does not substitute HTML error pages.","Optionally improve parseJsonOrThrow to append `: ${err.message}` and the response status to the thrown error so future hits are self-diagnosing."],"exampleFix":"// before\nasync function parseJsonOrThrow(response) {\n  let payload;\n  try {\n    payload = await response.json();\n  } catch (err) {\n    throw new Error('Server returned an invalid JSON response.');\n  }\n  // ...\n}\n\n// after - keep the cause and the status for diagnosis\nasync function parseJsonOrThrow(response) {\n  let payload;\n  const raw = await response.text();\n  try {\n    payload = JSON.parse(raw);\n  } catch (err) {\n    throw new Error(\n      `Server returned an invalid JSON response (status ${response.status}, content-type ${response.headers.get('content-type')}).`\n    );\n  }\n  // ...\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight: confirm the API is actually serving JSON before relying on it\nasync function apiIsHealthy(baseUrl = '') {\n  const probe = await fetch(`${baseUrl}/api/v1/system`);\n  const type = probe.headers.get('content-type') || '';\n  return probe.ok && type.includes('application/json');\n}","typeGuard":null,"tryCatchPattern":"try {\n  const info = await fetchSystemInfo(simulation, signal);\n} catch (err) {\n  if (err.message === 'Server returned an invalid JSON response.') {\n    // transport/proxy problem, not a llmfit error - check server and proxy\n    showFatalError('Backend did not return JSON. Is the llmfit server running?');\n  }\n  throw err;\n}","preventionTips":["Run the dashboard from the llmfit server itself so /api/v1/* and the assets share one origin.","If using the Vite dev server, configure the /api proxy to the backend port and fail fast when the proxy target is down.","Keep an error boundary around data fetching that distinguishes 'invalid JSON' (transport) from status errors (API logic).","Never point the dashboard at a static file server; only the Axum process serves /api/v1."],"tags":["json","http","network","frontend","dashboard"],"backgroundTag":"invalid-json-response","analyzedSha":"a9ac7ed91c1729cd93944bc1338e8313daaba8fa","analyzedAt":"2026-08-16T19:19:11.438Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}