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

  1. Verify the file is a real zip (file budget.ynab4 or unzip -t); re-export from YNAB4 if it is not.
  2. Re-download the export and confirm the byte size matches the source; a truncated download is the most common cause.
  3. Ensure you pass the exported .ynab4 zip to parseFile, not the inner Budget.yfull or a JSON file.
  4. 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

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


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