{"record":{"id":"106029d02e6f8c4b","repo":"actualbudget/actual","slug":"fileaccesserror-requirefileaccess-denial","errorCode":null,"errorMessage":"fileAccessError (requireFileAccess denial)","messagePattern":"fileAccessError \\(requireFileAccess denial\\)","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"packages/sync-server/src/app-sync.ts","lineNumber":179,"sourceCode":"    return;\n  }\n\n  const filesService = new FilesService(getAccountDb());\n\n  const currentFile = verifyFileExists(\n    fileId,\n    filesService,\n    res,\n    'file-not-found',\n  );\n\n  if (!currentFile) {\n    return;\n  }\n\n  const fileAccessError = requireFileAccess(currentFile, res.locals.user_id);\n  if (fileAccessError) {\n    res.status(403);\n    res.send(fileAccessError);\n    return;\n  }\n\n  const errorMessage = validateSyncedFile(groupId, keyId, currentFile);\n  if (errorMessage) {\n    res.status(400);\n    res.send(errorMessage);\n    return;\n  }\n\n  const { trie, newMessages } = simpleSync.sync(messages, since, groupId);\n\n  const responsePb = create(SyncResponseSchema, {\n    merkle: JSON.stringify(trie),\n    messages: newMessages,\n  });\n","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-sync.ts#L161-L197","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ask the file owner to re-share the budget with your user on the sync server.","Sync with the account that owns the file.","Verify user_id/token pairing on self-hosted setups (re-create the user, re-login, and re-download the budget)."],"exampleFix":"// before: token for user A, fileId owned by user B -> 403 fileAccessError\n// after: login as the owning user (or be granted access), then sync\nawait actual.login({ password }); // as the account that owns the file\nawait actual.downloadBudget(fileId);","handlingStrategy":"try-catch","validationCode":"const files = await (await fetch(serverUrl + '/files', { headers: authHeaders })).json();\nif (!files.data.some(f => f.fileId === fileId)) throw new Error('user cannot access file ' + fileId + ' — sync would be denied');","typeGuard":null,"tryCatchPattern":"try {\n  const res = await sync(fileId);\n  if (res.status === 403) {\n    const err = await res.json();\n    throw new Error('file access denied: ' + JSON.stringify(err));\n  }\n  return res;\n} catch (e) { throw e; }","preventionTips":["Sync only with accounts that own or are shared the budget","Re-login after user accounts are recreated on self-hosted servers","Confirm sharing grants survive owner-side budget deletions/re-creations"],"tags":["auth","permissions","http-403","sync"],"backgroundTag":"insufficient-permissions","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}