{"record":{"id":"c3cdfe6684746a07","repo":"actualbudget/actual","slug":"error-reading-zip-file","errorCode":null,"errorMessage":"Error reading zip file","messagePattern":"Error reading zip file","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/importers/ynab4.ts","lineNumber":443,"sourceCode":"  return files[0];\n}\n\nfunction join(...paths: string[]): string {\n  return paths.slice(1).reduce(\n    (full, path) => {\n      return full + '/' + path.replace(/^\\//, '');\n    },\n    paths[0].replace(/\\/$/, ''),\n  );\n}\n\nexport function parseFile(buffer: Buffer): YNAB4.YFull {\n  let zipped: Record<string, Uint8Array>;\n  try {\n    zipped = safeUnzip(buffer);\n  } catch (e) {\n    logger.log(e);\n    throw new Error('Error reading zip file');\n  }\n  const entries = Object.keys(zipped);\n\n  let root = '';\n  const dirMatch = entries[0].match(/([^/]*\\.ynab4)/);\n  if (dirMatch) {\n    root = dirMatch[1] + '/';\n  }\n\n  const metaStr = Buffer.from(zipped[getFile(entries, root + 'Budget.ymeta')]);\n  const meta = JSON.parse(metaStr.toString('utf8'));\n  const budgetPath = join(root, meta.relativeDataFolderName);\n\n  const deviceFiles = entries.filter(e =>\n    e.startsWith(join(budgetPath, 'devices')),\n  );\n  const deviceGUID = findLatestDevice(zipped, deviceFiles);\n","sourceCodeStart":425,"sourceCodeEnd":461,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/importers/ynab4.ts#L425-L461","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait parseFile(await fs.readFile('budget.html'));\n// after\nconst buf = await fs.readFile('budget.ynab4');\nif (buf.subarray(0, 2).toString() !== 'PK') throw new Error('Not a zip file');\nawait parseFile(buf);","handlingStrategy":"validation","validationCode":"function looksLikeZip(buffer) {\n  return Buffer.isBuffer(buffer) && buffer.length >= 4 &&\n    buffer[0] === 0x50 && buffer[1] === 0x4b; // 'PK'\n}","typeGuard":"function isNonEmptyBuffer(v) {\n  return Buffer.isBuffer(v) && v.length > 0;\n}","tryCatchPattern":"try {\n  const budget = parseFile(buffer);\n} catch (e) {\n  if (e.message === 'Error reading zip file') {\n    // re-download / re-export; verify the file is a zip\n  } else throw e;\n}","preventionTips":["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."],"tags":["import","ynab4","zip","corrupt-file"],"backgroundTag":"corrupt-archive","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}