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

  1. Verify the Actual-Sync-Server URL in settings points at the real sync server API, not a landing page or dashboard URL.
  2. 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).
  3. Bypass intermediaries (VPN, captive portal, corporate proxy) or whitelist the sync server domain.
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/ec138c2b4c618ae3. Report an issue: GitHub.