actualbudget/actual · error

zipMeta ? getUnsafeZipError(zipMeta) : error

Error message

zipMeta ? getUnsafeZipError(zipMeta) : error

What it means

`importBudget` sends the file path to the backend via `import-budget`; if the backend returns an `error`, the thunk throws it as a JS Error. Before throwing it checks `getUnsafeZipMeta(meta)` — when the failure is a flagged unsafe zip archive (e.g. zip-slip path traversal or unsafe compression ratio), the error is replaced with the more specific `getUnsafeZipError(zipMeta)` message. Otherwise the raw backend error string is thrown.

Source

Thrown at packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts:250

        : new Error('Error duplicating budget: ' + String(error));
    } finally {
      dispatch(setAppState({ loadingText: null }));
    }
  },
);

type ImportBudgetPayload = {
  filepath: string;
  type: Parameters<Handlers['import-budget']>[0]['type'];
};

export const importBudget = createAppAsyncThunk(
  `${sliceName}/importBudget`,
  async ({ filepath, type }: ImportBudgetPayload, { dispatch }) => {
    const { error, meta } = await send('import-budget', { filepath, type });
    if (error) {
      const zipMeta = getUnsafeZipMeta(meta);
      throw new Error(zipMeta ? getUnsafeZipError(zipMeta) : error);
    }

    dispatch(closeModal());
    await dispatch(loadPrefs());
  },
);

type UploadBudgetPayload = {
  id?: string;
};

export const uploadBudget = createAppAsyncThunk(
  `${sliceName}/uploadBudget`,
  async ({ id }: UploadBudgetPayload, { dispatch }) => {
    const { error } = await send('upload-budget', { id });
    if (error) {
      return { error };
    }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Read the thrown message: if it is the unsafe-zip error, re-export the budget from the source application and avoid untrusted/hand-edited zip files.
  2. Verify the file path exists and is readable before importing; on desktop re-pick the file via the file dialog.
  3. Confirm the file type matches the `type` argument (e.g. 'ynab4', 'ynab5', 'actual') — importing with the wrong type fails validation.
  4. Try importing an uncompressed/known-good export to isolate whether the archive itself is the problem.

Example fix

// before
await dispatch(importBudget({ filepath: '/downloads/budget.zip', type: 'actual' }));
// after
const stat = await window.fs.stat(filepath); // or fs.existsSync in node context
if (!stat) { alert('File not found: ' + filepath); return; }
try {
  await dispatch(importBudget({ filepath, type: 'actual' })).unwrap();
} catch (e) {
  alert('Import failed: ' + (e instanceof Error ? e.message : String(e)));
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!filepath || !(await fileExists(filepath))) {
  alert('Import file not found: ' + filepath);
  return;
}
const lower = filepath.toLowerCase();
if (!['.zip', '.ynab', '.json', '.csv', '.ofx', '.qfx'].some(ext => lower.endsWith(ext))) {
  alert('Unsupported import file type.');
  return;
}

Type guard

function isImportFailure(
  e: unknown,
): e is Error & { message: string } {
  return e instanceof Error &&
    (e.message.includes('zip') || e.message.includes('import'));
}

Try / catch

try {
  await dispatch(importBudget({ filepath, type })).unwrap();
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  if (/zip/i.test(msg)) {
    alert('The archive is unsafe or corrupted. Re-export the budget and try again.');
  } else {
    alert('Import failed: ' + msg);
  }
}

Prevention

When it happens

Trigger: Calling `dispatch(importBudget({ filepath, type }))` when the backend import fails: the file at `filepath` is not a valid/recognizable budget or export format, the file is a malicious/unsafe zip archive (detected via zip metadata in `meta`), the path is unreadable, or the imported data fails backend validation — any case where `send('import-budget')` resolves with a truthy `error`.

Common situations: Importing a corrupted or truncated .zip budget export; importing a random zip that isn't an Actual export; importing YNAB4/YNAB5/Actual files with schema problems; on desktop, a file path that no longer exists or lacks read permission.

Related errors


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