actualbudget/actual · warning
user-already-have-access
user-already-have-access
Error message
User already have access
What it means
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.
Source
Thrown at packages/sync-server/src/app-admin.js:267
res.status(404).send({
status: 'error',
reason: 'invalid-file-id',
details: 'File not found at server',
});
return;
}
if (!userAccess.userId) {
res.status(400).send({
status: 'error',
reason: 'user-cant-be-empty',
details: 'User cannot be empty',
});
return;
}
if (UserService.countUserAccess(userAccess.fileId, userAccess.userId) > 0) {
res.status(400).send({
status: 'error',
reason: 'user-already-have-access',
details: 'User already have access',
});
return;
}
UserService.addUserAccess(userAccess.userId, userAccess.fileId);
res.status(200).send({ status: 'ok', data: {} });
});
app.delete('/access', (req, res) => {
const fileId = req.query.fileId;
const session = validateSession(req, res);
if (!session) return;
const { granted } = UserService.checkFilePermission(View on GitHub (pinned to d4334cb6e6)
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.
Example fix
// before
await api.post('/access', { fileId, userId });
// after
const existing = await api.get(`/access?fileId=${fileId}`);
if (!existing.some(u => u.userId === userId)) {
await api.post('/access', { fileId, userId });
} Defensive patterns
Strategy: validation
Validate before calling
async function grantIfNew(fileId, userId) {
const existing = await api.get(`/access?fileId=${fileId}`);
if (existing.some(u => u.userId === userId)) return { skipped: true };
return api.post('/access', { fileId, userId });
} Type guard
function alreadyHasAccess(users, userId) {
return Array.isArray(users) && users.some(u => u.userId === userId);
} Try / catch
try {
await api.post('/access', { fileId, userId });
} catch (e) {
if (e.response?.data?.reason === 'user-already-have-access') {
return { ok: true, alreadyShared: true }; // treat as idempotent success
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/9b25ca91d0b91506.
Report an issue: GitHub.