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.

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)

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.