{"record":{"id":"f01fba79949c555d","repo":"actualbudget/actual","slug":"not-all-deleted","errorCode":"not-all-deleted","errorMessage":"not-all-deleted","messagePattern":"not-all-deleted","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"packages/sync-server/src/app-admin.js","lineNumber":179,"sourceCode":"  const { ids } = req.body || {};\n  let totalDeleted = 0;\n  ids.forEach(item => {\n    const ownerId = UserService.getOwnerId();\n\n    if (item === ownerId) return;\n\n    UserService.deleteUserAccess(item);\n    UserService.transferAllFilesFromUser(ownerId, item);\n    const usersDeleted = UserService.deleteUser(item);\n    totalDeleted += usersDeleted;\n  });\n\n  if (ids.length === totalDeleted) {\n    res\n      .status(200)\n      .send({ status: 'ok', data: { someDeletionsFailed: false } });\n  } else {\n    res.status(400).send({\n      status: 'error',\n      reason: 'not-all-deleted',\n      details: '',\n    });\n  }\n});\n\napp.get('/access', validateSessionMiddleware, (req, res) => {\n  const fileId = req.query.fileId;\n\n  const { granted } = UserService.checkFilePermission(\n    fileId,\n    res.locals.user_id,\n  ) || {\n    granted: 0,\n  };\n\n  if (granted === 0 && !isAdmin(res.locals.user_id)) {","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-admin.js#L161-L197","documentation":"DELETE /users compares `ids.length` with `totalDeleted` returned by the deletion service. If any id could not be deleted (no matching row, constraints, or an internal failure), the handler responds 400 with reason 'not-all-deleted' and an empty details string, signaling a partial deletion instead of full success.","triggerScenarios":"DELETE /users (admin session) where the ids array contains at least one id absent from the users table (already deleted, mistyped, or from another environment), so totalDeleted < ids.length.","commonSituations":"Batch cleanup scripts built from stale user lists; double-invoked deletion where the second run's ids no longer exist; mixed-environment ids (staging ids sent to production); ids collected manually with typos.","solutions":["Re-list users via the admin API and retry deletion with only ids that still exist.","Treat the request as partially successful: query which users remain and delete them individually to surface the failing id.","Refresh the client's user list cache before building the ids array.","Make batch deletions idempotent — ignore 'missing' ids on retry."],"exampleFix":"// before\nawait deleteUsers({ ids: staleIds }); // some ids already gone\n// after\nconst users = await listUsers();\nconst liveIds = ids.filter(id => users.some(u => u.id === id));\nif (liveIds.length) await deleteUsers({ ids: liveIds });","handlingStrategy":"validation","validationCode":"const users = await listUsers();\nconst liveIds = ids.filter(id => users.some(u => u.id === id));\nif (liveIds.length !== ids.length) {\n  console.warn(`Skipping ${ids.length - liveIds.length} ids not present on server`);\n}\nif (liveIds.length) await deleteUsers({ ids: liveIds });","typeGuard":null,"tryCatchPattern":"try {\n  const res = await fetch(base + '/users', { method: 'DELETE', ... });\n  const body = await res.json();\n  if (body.reason === 'not-all-deleted') {\n    // re-list users, delete remaining matching ids one by one to isolate failures\n  }\n} catch (e) { /* transport error */ }","preventionTips":["Build batch id lists from a fresh server listing, not stale caches.","Design deletions to be idempotent and tolerant of missing ids.","Avoid re-running the same delete batch without re-checking state.","Delete users individually when you need per-id error attribution."],"tags":["http-400","partial-failure","batch","sync-server"],"backgroundTag":"partial-batch-failure","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}