{"record":{"id":"9b25ca91d0b91506","repo":"actualbudget/actual","slug":"user-already-have-access","errorCode":"user-already-have-access","errorMessage":"User already have access","messagePattern":"User already have access","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"packages/sync-server/src/app-admin.js","lineNumber":267,"sourceCode":"    res.status(404).send({\n      status: 'error',\n      reason: 'invalid-file-id',\n      details: 'File not found at server',\n    });\n    return;\n  }\n\n  if (!userAccess.userId) {\n    res.status(400).send({\n      status: 'error',\n      reason: 'user-cant-be-empty',\n      details: 'User cannot be empty',\n    });\n    return;\n  }\n\n  if (UserService.countUserAccess(userAccess.fileId, userAccess.userId) > 0) {\n    res.status(400).send({\n      status: 'error',\n      reason: 'user-already-have-access',\n      details: 'User already have access',\n    });\n    return;\n  }\n\n  UserService.addUserAccess(userAccess.userId, userAccess.fileId);\n\n  res.status(200).send({ status: 'ok', data: {} });\n});\n\napp.delete('/access', (req, res) => {\n  const fileId = req.query.fileId;\n  const session = validateSession(req, res);\n  if (!session) return;\n\n  const { granted } = UserService.checkFilePermission(","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-admin.js#L249-L285","documentation":"Returned by POST /access with HTTP 400 and reason 'user-already-have-access' when UserService.countUserAccess(fileId, userId) is greater than 0, meaning the target user already has an access row for that file. The server enforces idempotency: duplicate grants are rejected instead of silently ignored.","triggerScenarios":"POST /access with {fileId, userId} for a pair that already exists in the user access table — e.g. clicking 'share' twice, a retried request after a timeout, or seeding script run more than once.","commonSituations":"Double-click on the share button; automated retry logic without idempotency keys; batch import scripts re-run after partial failure; UI list not refreshed so the user appears unshared.","solutions":["Check existing access (GET /access?fileId=...) before POSTing and skip if the user is already listed.","Treat reason 'user-already-have-access' as success in idempotent clients.","Add a guard in scripts so a user/file pair is only submitted once."],"exampleFix":"// before\nawait api.post('/access', { fileId, userId });\n// after\nconst existing = await api.get(`/access?fileId=${fileId}`);\nif (!existing.some(u => u.userId === userId)) {\n  await api.post('/access', { fileId, userId });\n}","handlingStrategy":"validation","validationCode":"async function grantIfNew(fileId, userId) {\n  const existing = await api.get(`/access?fileId=${fileId}`);\n  if (existing.some(u => u.userId === userId)) return { skipped: true };\n  return api.post('/access', { fileId, userId });\n}","typeGuard":"function alreadyHasAccess(users, userId) {\n  return Array.isArray(users) && users.some(u => u.userId === userId);\n}","tryCatchPattern":"try {\n  await api.post('/access', { fileId, userId });\n} catch (e) {\n  if (e.response?.data?.reason === 'user-already-have-access') {\n    return { ok: true, alreadyShared: true }; // treat as idempotent success\n  }\n  throw e;\n}","preventionTips":["Check the current access list before granting.","Make share actions idempotent in the UI (disable button after first click).","Dedupe user/file pairs in batch scripts."],"tags":["duplicate","http-400","sync-server","idempotency"],"backgroundTag":"duplicate-resource","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}