actualbudget/actual · error · Error

getDownloadError(result.error)

Error message

getDownloadError(result.error)

What it means

Thrown by api/download-budget in packages/loot-core/src/server/api.ts when handlers['download-budget'] fails to fetch and import the remote file. getDownloadError() (packages/loot-core/src/shared/errors.ts:76) maps reasons — network/download-failure, invalid zip/meta, zip-too-large, decrypt-failure, out-of-sync-migrations, clock-drift — to actionable messages, and the reason becomes the error code. Full details are logged via logger before throwing.

Solutions

  1. Check the logged 'Full error details' and the error.code for the precise reason
  2. For network/download-failure, verify server health and retry
  3. For decrypt-failure, supply the correct password to downloadBudget
  4. For zip-too-large/invalid-zip, inspect the server's stored file and restore a clean copy from a working client

Example fix

// before
await api.downloadBudget(syncId);
// after
try {
  await api.downloadBudget(syncId, { password });
} catch (e) {
  if (['network', 'download-failure'].includes(e.code)) await retry(() => api.downloadBudget(syncId, { password }), 3);
  else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

const res = await fetch(`${serverUrl}/health`);
if (!res.ok) throw new Error('Server unhealthy before download');

Type guard

function isDownloadReason(r: string): boolean {
  return ['network','download-failure','not-zip-file','invalid-zip-file','invalid-meta-file','zip-too-large','decrypt-failure','out-of-sync-migrations','clock-drift'].includes(r);
}

Try / catch

try {
  await api.downloadBudget(syncId, { password });
} catch (e) {
  if (['network', 'download-failure'].includes(e.code)) {
    await backoffRetry(() => api.downloadBudget(syncId, { password }), 3);
  } else if (e.code === 'decrypt-failure') {
    throw new Error('Bad password: ' + getDownloadError(e));
  } else throw e;
}

Prevention

When it happens

Trigger: downloadBudget(syncId) reaching the actual cloud download step and failing: server unreachable mid-transfer, corrupted/partial zip stored server-side, archive exceeding size limits, wrong password for E2E-encrypted file, or clock drift detected during transfer.

Common situations: Self-hosted server data directory with truncated .zip files; budget exceeding max file size; unstable network in CI pipelines; encryption password changed between key-test and download; container clock skew.

Related errors


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

Appendix: source

Thrown at packages/loot-core/src/server/api.ts:258

    const result = await handlers['sync-budget']();
    if (result.error) {
      throw withErrorCode(
        new Error(
          getSyncError(result.error.reason, localBudget.id, result.error.meta),
        ),
        result.error.reason,
      );
    }
    return;
  }

  // Download the remote file (no need to perform a sync as the file will already be up-to-date)
  const result = await handlers['download-budget']({
    cloudFileId: remoteBudget.fileId,
  });
  if (result.error) {
    logger.log('Full error details', result.error);
    throw withErrorCode(
      new Error(getDownloadError(result.error)),
      result.error.reason,
    );
  }
  await handlers['load-budget']({ id: result.id });
};

handlers['api/get-budgets'] = async function () {
  const budgets = await handlers['get-budgets']();
  const files = (await handlers['get-remote-files']()) || [];
  return [
    ...budgets.map(file => budgetModel.toExternal(file)),
    ...files.map(file => remoteFileModel.toExternal(file)).filter(file => file),
  ];
};

handlers['api/sync'] = async function () {
  const { id } = prefs.getPrefs();

View on GitHub (pinned to d4334cb6e6)