actualbudget/actual · error

file-not-found

file-not-found

Error message

file-not-found

What it means

When the fileId is valid but filesService.get cannot find the file (throws FileNotFound), verifyFileExists responds 400 with the caller-supplied errorObject whose code is 'file-not-found'. Comments in the source note this should ideally be 404; it is kept 400 for frontend compatibility.

Source

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

const verifyFileExists = (
  fileId: unknown,
  filesService: FilesService,
  res: Response,
  errorObject: string | Record<string, unknown>,
) => {
  if (typeof fileId !== 'string' || !isValidFileId(fileId)) {
    res.status(400).send('invalid fileId');
    return;
  }

  try {
    return filesService.get(fileId);
  } catch (e) {
    if (e instanceof FileNotFound) {
      //FIXME: error code should be 404. Need to make sure frontend is ok with it.
      //TODO: put this into a middleware that checks if FileNotFound is thrown and returns 404 and same error message
      // for every FileNotFound error
      res.status(400).send(errorObject);
      return;
    }
    throw e;
  }
};

function requireFileOwner(file: File, userId: string) {
  const isOwner = file.owner === userId;
  const isServerAdmin = isAdmin(userId);
  if (isOwner || isServerAdmin) {
    return null;
  }
  return 'file-access-not-allowed';
}

function requireFileAccess(file: File, userId: string) {
  if (requireFileOwner(file, userId) === null) {
    return null;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the server URL matches the environment that owns the budget, then re-upload/re-create the budget or restore from backup.
  2. List files on the server and use an existing fileId.
  3. If the budget is gone, create a new one and let the client upload it (full sync) instead of syncing the stale id.

Example fix

// before
await fetch(`${serverUrl}/sync/file?fileId=${oldFileId}`); // 400 file-not-found
// after: check existence first
const files = await (await fetch(`${serverUrl}/files`, { headers: authHeaders })).json();
const exists = files.data.some(f => f.fileId === oldFileId);
if (!exists) await actual.createBudget({}); // or pick an existing fileId
Defensive patterns

Strategy: validation

Validate before calling

const files = await (await fetch(serverUrl + '/files', { headers: authHeaders })).json();
if (!files.data.some(f => f.fileId === fileId)) throw new Error('file ' + fileId + ' does not exist on this server');

Try / catch

try {
  const res = await fetch(`${serverUrl}/sync/file?fileId=${fileId}`, { headers: authHeaders });
  if (res.status === 400) {
    const err = await res.json();
    if (err.code === 'file-not-found' || err.reason === 'file-not-found') throw new Error('budget missing on server: re-upload or restore it');
  }
  return res;
} catch (e) { throw e; }

Prevention

When it happens

Trigger: A /sync/file, /sync/currentfile, or /sync call referencing a fileId that does not exist on this server — deleted budget, wrong server environment, or a file id from a different instance.

Common situations: Pointing a client at a fresh sync-server without importing/restoring the budget; switching between self-hosted and the cloud server; a budget deleted while another device still syncs it.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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