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

  1. Start/restart the sync server behind the tunnel and verify it responds locally (curl localhost:5006)
  2. Confirm the tunnel forwards to the correct port
  3. Check tunnel provider dashboard for upstream health errors
  4. 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

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


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