actualbudget/actual · error · FileDownloadError

download-failure

download-failure

Error message

download-failure

What it means

FileDownloadError('download-failure') is thrown when the HTTP fetch of /download-user-file fails — either a non-OK HTTP status (checkHTTPStatus) or a network/transport error while reading the body into a Buffer. The budget archive was never retrieved.

Source

Thrown at packages/loot-core/src/server/cloud-storage.ts:463

  const userToken = await asyncStorage.getItem('user-token');
  const syncServer = getServer().SYNC_SERVER;

  const userFileFetch = fetch(`${syncServer}/download-user-file`, {
    headers: {
      'X-ACTUAL-TOKEN': userToken,
      'X-ACTUAL-FILE-ID': cloudFileId,
    },
  })
    .then(checkHTTPStatus)
    .then(res => {
      if (res.arrayBuffer) {
        return res.arrayBuffer().then(ab => Buffer.from(ab));
      }
      return res.buffer();
    })
    .catch(err => {
      logger.log('Download failure', err);
      throw FileDownloadError('download-failure');
    });

  const userFileInfoFetch = fetchJSON(`${syncServer}/get-user-file-info`, {
    headers: {
      'X-ACTUAL-TOKEN': userToken,
      'X-ACTUAL-FILE-ID': cloudFileId,
    },
  }).catch(err => {
    logger.log('Error fetching file info', err);
    throw FileDownloadError('internal', { fileId: cloudFileId });
  });

  const [userFileInfoRes, userFileRes] = await Promise.all([
    userFileInfoFetch,
    userFileFetch,
  ]);

  if (userFileInfoRes.status !== 'ok') {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the cloudFileId exists via listRemoteFiles before downloading
  2. Sign in again to refresh the user-token, then retry
  3. Check the sync server is running and reachable; retry on transient network errors
  4. Check server logs for the /download-user-file request to see the actual HTTP error

Example fix

// before
await api.downloadBudget(cloudFileId);
// after
const files = await listRemoteFiles();
if (!files?.some(f => f.cloudFileId === cloudFileId)) {
  throw new Error(`file ${cloudFileId} not on server`);
}
await api.downloadBudget(cloudFileId);
Defensive patterns

Strategy: validation

Validate before calling

async function assert downloadable(cloudFileId, token, syncUrl) {
  const files = await listRemoteFiles();
  if (!files?.some(f => f.cloudFileId === cloudFileId)) {
    throw new Error(`cloudFileId ${cloudFileId} not present on server`);
  }
}

Type guard

function fileExistsRemotely(files, cloudFileId) {
  return Array.isArray(files) && files.some(f => f?.cloudFileId === cloudFileId);
}

Try / catch

try {
  await download(cloudFileId);
} catch (e) {
  if (e instanceof FileDownloadError && e.reason === 'download-failure') {
    await backoffRetry(() => download(cloudFileId), 3); // transient HTTP/network failure
  } else throw e;
}

Prevention

When it happens

Trigger: download() where the sync server returns 4xx/5xx for the file download (bad token, unknown fileId, server error) or the connection drops mid-download.

Common situations: wrong cloudFileId passed to api.downloadBudget, expired token, self-hosted server offline, proxy timeouts on large budgets, server deleting the file between listing and download.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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