actualbudget/actual · error · PostError
parse-json
parse-json
Error message
parse-json
What it means
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.
Source
Thrown at packages/loot-core/src/server/post.ts:102
externalSignal?.aborted
) {
throw new PostError('aborted');
}
throw new PostError('network-failure', undefined, { cause: err });
} finally {
if (timeoutId != null) clearTimeout(timeoutId);
externalSignal?.removeEventListener('abort', onExternalAbort);
}
throwIfNot200(res, text);
let responseData;
try {
responseData = JSON.parse(text);
} catch {
// Something seriously went wrong. TODO handle errors
throw new PostError('parse-json', { meta: text });
}
if (responseData.status !== 'ok') {
logger.log(
'API call failed: ' +
url +
'\nData: ' +
JSON.stringify(data, null, 2) +
'\nResponse: ' +
JSON.stringify(res, null, 2),
);
throw new PostError(
responseData.description || responseData.reason || 'unknown',
);
}
return responseData.data;View on GitHub (pinned to d4334cb6e6)
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.
Example fix
// before: generic URL, proxy serves HTML with 200 const server = 'https://myapp.example.com'; // after: explicit sync-server API origin const server = 'https://sync.example.com'; // actual sync-server, responds JSON
Defensive patterns
Strategy: try-catch
Validate before calling
const server = await getServerUrl();
if (!server || !/^https?:\/\//.test(server)) throw new Error('sync server URL not configured');
// optionally probe: const probe = await fetch(server + '/health'); const ct = probe.headers.get('content-type') || '';
// if (!ct.includes('application/json')) throw new Error('server not returning JSON'); Type guard
function isPostError(e: unknown): e is { reason: string; details?: unknown } {
return typeof e === 'object' && e !== null && 'reason' in e && (e as any).reason === 'parse-json';
} Try / catch
try {
await post(server + '/some-endpoint', data);
} catch (e) {
if (isPostError(e) && e.reason === 'parse-json') {
// e.details.meta holds the raw non-JSON body: log and surface a config/proxy problem
logger.log('Non-JSON response from sync server:', e.details);
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- No sync server configured.
- getServerErrorReason(json)
- network-failure
- Blocked request to private/local IP: ${hostname}
- Unable to resolve host: ${hostname}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/ec138c2b4c618ae3.
Report an issue: GitHub.