actualbudget/actual · error · PostError
getServerErrorReason(json)
Error message
getServerErrorReason(json)
What it means
PostError carrying the server-provided error 'reason' extracted from a JSON error body (getServerErrorReason) when a sync response is non-200 with Content-Type application/json. The message shown is a placeholder for whatever reason string the server sent (e.g. 'file-has-changed', 'unauthorized').
Source
Thrown at packages/loot-core/src/server/post.ts:23
import { PostError } from './errors';
export function getServerErrorReason(error) {
return error.reason === 'unauthorized' && error.details === 'token-not-found'
? 'token-expired'
: error.reason;
}
function throwIfNot200(res: Response, text: string) {
if (res.status !== 200) {
if (res.status === 500) {
throw new PostError(res.status === 500 ? 'internal' : text);
}
const contentType = res.headers.get('Content-Type') ?? '';
if (contentType.toLowerCase().indexOf('application/json') !== -1) {
const json = JSON.parse(text);
throw new PostError(getServerErrorReason(json));
}
// Actual Sync Server may be exposed via a tunnel (e.g. ngrok). Tunnel errors should be treated as network errors.
const tunnelErrorHeaders = ['ngrok-error-code'];
const tunnelError = tunnelErrorHeaders.some(header =>
res.headers.has(header),
);
if (tunnelError) {
// Tunnel errors are present when the tunnel is active and the server is not reachable e.g. server is offline
// When we experience a tunnel error we treat it as a network failure
throw new PostError('network-failure');
}
throw new PostError(text);
}
}
View on GitHub (pinned to d4334cb6e6)
Solutions
- Read the PostError's message/reason field to see the server's actual reason and act on it
- For 'unauthorized', re-set the sync credentials (server URL, password)
- For 'file-has-changed', re-download the budget or force re-sync
- Ensure client and server versions are compatible
Example fix
// before
try { await post(url, data); } catch (e) { console.error(e); }
// after
try { await post(url, data); }
catch (e) {
if (e instanceof PostError && e.message === 'file-has-changed') {
await forceDownload();
} else { throw e; }
} Defensive patterns
Strategy: try-catch
Type guard
function isPostErrorWithReason(e: unknown, reason: string): e is PostError {
return e instanceof PostError && e.message === reason;
} Try / catch
try {
await post(url, data);
} catch (e) {
if (e instanceof PostError) {
switch (e.message) {
case 'unauthorized': return reauth();
case 'file-has-changed': return handleConflict();
default: throw e;
}
}
throw e;
} Prevention
- Always handle known server reasons explicitly (unauthorized, file-has-changed, etc.)
- Keep sync credentials/passwords up to date
- Match client and server versions to avoid rejection
- Log the PostError reason for diagnostics before swallowing it
When it happens
Trigger: Any post/del/patch/postBinary call receiving a non-200 (non-500) JSON response, e.g. 401 invalid token, 409 file-has-changed during sync, 400 invalid request.
Common situations: Expired or wrong encryption/sync password, another device modified the file causing file-has-changed, self-hosted server rejecting an old client, bad budget id in hand-rolled API calls.
Related errors
- Failed to fetch catalog: ${response.statusText}
- Failed to fetch CSS from ${url}: ${response.status} ${respon
- ${text}
- No sync server configured.
- network-failure
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/67c2f43001201cda.
Report an issue: GitHub.