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

  1. Check existing access (GET /access?fileId=...) before POSTing and skip if the user is already listed.
  2. Treat reason 'user-already-have-access' as success in idempotent clients.
  3. 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

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.