{"record":{"id":"ec138c2b4c618ae3","repo":"actualbudget/actual","slug":"parse-json","errorCode":"parse-json","errorMessage":"parse-json","messagePattern":"parse-json","errorType":"error_code","errorClass":"PostError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/post.ts","lineNumber":102,"sourceCode":"      externalSignal?.aborted\n    ) {\n      throw new PostError('aborted');\n    }\n    throw new PostError('network-failure', undefined, { cause: err });\n  } finally {\n    if (timeoutId != null) clearTimeout(timeoutId);\n    externalSignal?.removeEventListener('abort', onExternalAbort);\n  }\n\n  throwIfNot200(res, text);\n\n  let responseData;\n\n  try {\n    responseData = JSON.parse(text);\n  } catch {\n    // Something seriously went wrong. TODO handle errors\n    throw new PostError('parse-json', { meta: text });\n  }\n\n  if (responseData.status !== 'ok') {\n    logger.log(\n      'API call failed: ' +\n        url +\n        '\\nData: ' +\n        JSON.stringify(data, null, 2) +\n        '\\nResponse: ' +\n        JSON.stringify(res, null, 2),\n    );\n\n    throw new PostError(\n      responseData.description || responseData.reason || 'unknown',\n    );\n  }\n\n  return responseData.data;","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/post.ts#L84-L120","documentation":"This PostError with code 'parse-json' is thrown by the `post` helper in packages/loot-core/src/server/post.ts:102 when the sync server responded with HTTP 200 but its body is not valid JSON. The raw response text is attached as `meta` on the error for debugging. It indicates the endpoint returned a 200 with a non-JSON payload (HTML error page, empty body, proxy interstitial), which Actual cannot interpret.","triggerScenarios":"Calling post() (via checkKey, resetSyncState, removeFile, or the budget 'error' flow) where the server returns status 200 but text that JSON.parse cannot parse — e.g. an empty body, an HTML login/captive-portal page, or a reverse-proxy response masquerading as 200.","commonSituations":"Sync server URL pointed at a wrong host that answers 200 with HTML (dashboard, captive portal); a corporate proxy or Cloudflare interstitial intercepting requests; a custom/older sync server build returning a non-JSON 200; hosting misconfig where the app server serves a landing page at the API path.","solutions":["Verify the Actual-Sync-Server URL in settings points at the real sync server API, not a landing page or dashboard URL.","Inspect error.details/meta (the raw response text in the PostError) to see what the server actually returned; fix whatever produced that payload (proxy, redirect, auth wall).","Bypass intermediaries (VPN, captive portal, corporate proxy) or whitelist the sync server domain.","Confirm the server is an up-to-date actualbudget sync-server release; upgrade if it is a custom or stale build."],"exampleFix":"// before: generic URL, proxy serves HTML with 200\nconst server = 'https://myapp.example.com';\n// after: explicit sync-server API origin\nconst server = 'https://sync.example.com'; // actual sync-server, responds JSON","handlingStrategy":"try-catch","validationCode":"const server = await getServerUrl();\nif (!server || !/^https?:\\/\\//.test(server)) throw new Error('sync server URL not configured');\n// optionally probe: const probe = await fetch(server + '/health'); const ct = probe.headers.get('content-type') || '';\n// if (!ct.includes('application/json')) throw new Error('server not returning JSON');","typeGuard":"function isPostError(e: unknown): e is { reason: string; details?: unknown } {\n  return typeof e === 'object' && e !== null && 'reason' in e && (e as any).reason === 'parse-json';\n}","tryCatchPattern":"try {\n  await post(server + '/some-endpoint', data);\n} catch (e) {\n  if (isPostError(e) && e.reason === 'parse-json') {\n    // e.details.meta holds the raw non-JSON body: log and surface a config/proxy problem\n    logger.log('Non-JSON response from sync server:', e.details);\n  }\n  throw e;\n}","preventionTips":["Point the client at the sync server API origin, never a landing page or dashboard URL.","Keep reverse proxies/CDNs from intercepting or rewriting the sync server paths.","Probe the endpoint's Content-Type when configuring a new server URL.","Include the raw response text in your logs when wrapping sync calls."],"tags":["network","json","sync-server","http-response"],"backgroundTag":"invalid-json-response","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}