actualbudget/actual · error

fileAccessError (requireFileAccess denial)

Error message

fileAccessError (requireFileAccess denial)

What it means

After the file is resolved, requireFileAccess checks that the authenticated user has permission over the budget file. On denial the server sends a 403 with the returned fileAccessError object describing why access was refused.

Source

Thrown at packages/sync-server/src/app-sync.ts:179

    return;
  }

  const filesService = new FilesService(getAccountDb());

  const currentFile = verifyFileExists(
    fileId,
    filesService,
    res,
    'file-not-found',
  );

  if (!currentFile) {
    return;
  }

  const fileAccessError = requireFileAccess(currentFile, res.locals.user_id);
  if (fileAccessError) {
    res.status(403);
    res.send(fileAccessError);
    return;
  }

  const errorMessage = validateSyncedFile(groupId, keyId, currentFile);
  if (errorMessage) {
    res.status(400);
    res.send(errorMessage);
    return;
  }

  const { trie, newMessages } = simpleSync.sync(messages, since, groupId);

  const responsePb = create(SyncResponseSchema, {
    merkle: JSON.stringify(trie),
    messages: newMessages,
  });

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ask the file owner to re-share the budget with your user on the sync server.
  2. Sync with the account that owns the file.
  3. Verify user_id/token pairing on self-hosted setups (re-create the user, re-login, and re-download the budget).

Example fix

// before: token for user A, fileId owned by user B -> 403 fileAccessError
// after: login as the owning user (or be granted access), then sync
await actual.login({ password }); // as the account that owns the file
await actual.downloadBudget(fileId);
Defensive patterns

Strategy: try-catch

Validate before calling

const files = await (await fetch(serverUrl + '/files', { headers: authHeaders })).json();
if (!files.data.some(f => f.fileId === fileId)) throw new Error('user cannot access file ' + fileId + ' — sync would be denied');

Try / catch

try {
  const res = await sync(fileId);
  if (res.status === 403) {
    const err = await res.json();
    throw new Error('file access denied: ' + JSON.stringify(err));
  }
  return res;
} catch (e) { throw e; }

Prevention

When it happens

Trigger: A /sync (or currentFile/file) call where the fileId resolves via filesService.get but res.locals.user_id is not the owner and has no share/access grant for that file.

Common situations: Syncing a budget after being unshared by the owner; using a server account that is not the budget owner; a user id mismatch after re-creating accounts on a self-hosted server; tokens issued for a different user on multi-user instances.

Related errors


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