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
- Verify the cloudFileId exists via listRemoteFiles before downloading
- Sign in again to refresh the user-token, then retry
- Check the sync server is running and reachable; retry on transient network errors
- 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
- Validate the fileId against listRemoteFiles before downloading
- Retry transient failures with backoff
- Keep sessions fresh to avoid 401s on large slow downloads
- Check server/proxy timeouts for big budget archives
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
- Failed to fetch catalog: ${response.statusText}
- Failed to fetch CSS from ${url}: ${response.status} ${respon
- ${text}
- getServerErrorReason(json)
- text (raw server response body)
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/932761a1d7d4424c.
Report an issue: GitHub.