actualbudget/actual · error · Error
Error reading zip file
Error message
Error reading zip file
What it means
parseFile wraps the unzip step (safeUnzip) in a try/catch and rethrows this generic error when the buffer cannot be read as a zip archive. The original error is logged, but the thrown error is deliberately opaque so callers get a consistent message.
Source
Thrown at packages/loot-core/src/server/importers/ynab4.ts:443
return files[0];
}
function join(...paths: string[]): string {
return paths.slice(1).reduce(
(full, path) => {
return full + '/' + path.replace(/^\//, '');
},
paths[0].replace(/\/$/, ''),
);
}
export function parseFile(buffer: Buffer): YNAB4.YFull {
let zipped: Record<string, Uint8Array>;
try {
zipped = safeUnzip(buffer);
} catch (e) {
logger.log(e);
throw new Error('Error reading zip file');
}
const entries = Object.keys(zipped);
let root = '';
const dirMatch = entries[0].match(/([^/]*\.ynab4)/);
if (dirMatch) {
root = dirMatch[1] + '/';
}
const metaStr = Buffer.from(zipped[getFile(entries, root + 'Budget.ymeta')]);
const meta = JSON.parse(metaStr.toString('utf8'));
const budgetPath = join(root, meta.relativeDataFolderName);
const deviceFiles = entries.filter(e =>
e.startsWith(join(budgetPath, 'devices')),
);
const deviceGUID = findLatestDevice(zipped, deviceFiles);
View on GitHub (pinned to d4334cb6e6)
Solutions
- Verify the file is a real zip (file budget.ynab4 or unzip -t); re-export from YNAB4 if it is not.
- Re-download the export and confirm the byte size matches the source; a truncated download is the most common cause.
- Ensure you pass the exported .ynab4 zip to parseFile, not the inner Budget.yfull or a JSON file.
- Check server logs for the logged original exception to identify the exact unzip failure.
Example fix
// before
await parseFile(await fs.readFile('budget.html'));
// after
const buf = await fs.readFile('budget.ynab4');
if (buf.subarray(0, 2).toString() !== 'PK') throw new Error('Not a zip file');
await parseFile(buf); Defensive patterns
Strategy: validation
Validate before calling
function looksLikeZip(buffer) {
return Buffer.isBuffer(buffer) && buffer.length >= 4 &&
buffer[0] === 0x50 && buffer[1] === 0x4b; // 'PK'
} Type guard
function isNonEmptyBuffer(v) {
return Buffer.isBuffer(v) && v.length > 0;
} Try / catch
try {
const budget = parseFile(buffer);
} catch (e) {
if (e.message === 'Error reading zip file') {
// re-download / re-export; verify the file is a zip
} else throw e;
} Prevention
- Verify downloads complete (compare file size/checksum) before importing.
- Check the magic bytes ('PK') before calling parseFile.
- Never pass HTML error pages or the inner .yfull file to parseFile.
- Keep the original export file; avoid opening it in tools that may rewrite it.
When it happens
Trigger: Calling parseFile(buffer) with a buffer that is not a valid zip: an HTML/text error page, a truncated download, a YNAB5 JSON export, or a zero-byte file.
Common situations: Downloaded export saved as HTML due to auth redirect; file truncated mid-upload; user selected the .yfull file itself instead of the exported .ynab4 zip; corrupted backup on disk.
Related errors
- Could not find file: ${path}
- File name matches multiple files: ${path}
- Error reading Budget.yfull file
- Error parsing Budget.yfull file
- zipMeta ? getUnsafeZipError(zipMeta) : error
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/c3cdfe6684746a07.
Report an issue: GitHub.