actualbudget/actual · critical · PostError
network-failure
network-failure
Error message
network-failure
What it means
PostError thrown when a sync response carries a tunnel error header (e.g. ngrok-error-code), meaning the tunnel is up but the backing sync server is unreachable. It is mapped to the sentinel message 'network-failure' so callers treat it like any connectivity outage.
Source
Thrown at packages/loot-core/src/server/post.ts:35
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);
}
}
export async function post(
url: RequestInfo,
data: unknown,
headers = {},
timeout: number | null = null,
// Optional caller-provided abort signal. Used by Enable Banking poll
// cancellation so the user can interrupt the 5-minute long-poll.
externalSignal?: AbortSignal | null,
) {
let text: string;
let res: Response;
View on GitHub (pinned to d4334cb6e6)
Solutions
- Start/restart the sync server behind the tunnel and verify it responds locally (curl localhost:5006)
- Confirm the tunnel forwards to the correct port
- Check tunnel provider dashboard for upstream health errors
- Retry sync once the server is reachable
Defensive patterns
Strategy: retry
Validate before calling
// before syncing, check the backing server is reachable
const health = await fetch(serverUrl.replace(/\/$/, '') + '/health');
if (!health.ok) throw new Error('Sync server behind tunnel is down'); Type guard
function isTunnelNetworkFailure(e: unknown): e is PostError {
return e instanceof PostError && e.message === 'network-failure';
} Try / catch
try {
await post(url, data);
} catch (e) {
if (isTunnelNetworkFailure(e)) {
await waitForServer(serverUrl); // poll until tunnel upstream is back
return retry(post, [url, data]);
}
throw e;
} Prevention
- Run the sync server as a supervised service (systemd/docker restart policy) so it survives crashes
- Health-check the upstream server independently of the tunnel
- Verify tunnel port mapping after every config change
- Alert on tunnel error headers in any proxy logs you control
When it happens
Trigger: Accessing a self-hosted sync server through ngrok (or similar) while the local Actual server process is stopped or crashed; the tunnel returns an error page with the tunnel header, non-200.
Common situations: ngrok tunnel running but the actual sync-server not started, server container crashed, wrong port forwarded by the tunnel, tunnel pointing at a stale upstream.
Related errors
- No sync server configured.
- getServerErrorReason(json)
- parse-json
- 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/ac5198fe4751eb62.
Report an issue: GitHub.