{"record":{"id":"1d7444052167df3e","repo":"odysseus-dev/odysseus","slug":"cannot-rename-user","errorCode":null,"errorMessage":"Cannot rename user","messagePattern":"Cannot rename user","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":330,"sourceCode":"            raise HTTPException(403, \"Admin only\")\n        old_username = (username or \"\").strip().lower()\n        new_username = (body.username or \"\").strip().lower()\n        if not new_username:\n            raise HTTPException(400, \"Username required\")\n        if old_username == new_username:\n            return {\"ok\": True, \"username\": new_username, \"renamed_self\": old_username == user}\n        if old_username not in auth_manager.users:\n            raise HTTPException(404, \"User not found\")\n        if new_username in auth_manager.users:\n            raise HTTPException(409, \"Username already taken\")\n\n        # Gate on auth first. Every mutation below is contingent on this\n        # succeeding — doing it last meant a rejected rename (e.g. reserved\n        # username) left file-backed owner fields already rewritten with no\n        # way to roll them back.\n        ok = auth_manager.rename_user(old_username, new_username, user)\n        if not ok:\n            raise HTTPException(400, \"Cannot rename user\")\n\n        def _rollback_auth_rename() -> bool:\n            # On self-rename the admin session has already moved to the new\n            # username, so the rollback must authenticate as the new user.\n            rollback_user = new_username if user == old_username else user\n            try:\n                return bool(auth_manager.rename_user(new_username, old_username, rollback_user))\n            except Exception as rollback_err:\n                logger.error(\n                    \"Failed to roll back auth rename %s -> %s after owner migration failure: %s\",\n                    new_username, old_username, rollback_err,\n                )\n                return False\n\n        # Usernames are ownership keys for user data. Rename the common\n        # owner-scoped DB rows so the account keeps access to its sessions,\n        # docs, email accounts, tasks, etc.\n        try:","sourceCodeStart":312,"sourceCodeEnd":348,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L312-L348","documentation":"Raised by PUT /users/{username}/rename when auth_manager.rename_user() returns False after the route's own pre-checks passed. Looking at core/auth.py:341, the remaining failure causes are: new_username is in RESERVED_USERNAMES (the route does NOT pre-check this), the requesting user lost admin between the route gate and the manager call, or old/new vanished in a race. Note the route uses 400 here, though reserved-name is conceptually the same case as error 220's 403.","triggerScenarios":"Renaming a user to 'system', 'api', 'demo', or the internal tool user name (most common cause); requesting admin demoted concurrently; target user deleted concurrently after the route's existence check.","commonSituations":"Renaming service-style accounts onto reserved names during cleanup; admins acting simultaneously from two sessions.","solutions":["Avoid the reserved names {internal tool user, api, demo, system} (any case) as the new username.","Re-fetch GET /users, confirm your own admin status and the target's existence, then retry with a non-reserved name.","If you must claim a reserved-looking name, choose a variant like 'svc-system'."],"exampleFix":"// before\nawait api.put(`/users/${old}/rename`, { username: 'api' });\n// after\nconst RESERVED = ['system', 'api', 'demo']; // + internal tool user\nconst next = RESERVED.includes(newName.toLowerCase()) ? `user-${newName}` : newName;\nawait api.put(`/users/${old}/rename`, { username: next });","handlingStrategy":"validation","validationCode":"RESERVED = {'system', 'api', 'demo'}  # + internal tool user\nif new.strip().lower() in RESERVED:\n    raise ValueError('target username is reserved')","typeGuard":null,"tryCatchPattern":"try:\n    put(f'/users/{old}/rename', {'username': new})\nexcept HTTPError as e:\n    if e.response.status_code == 400 and 'Cannot rename' in e.response.json()['detail']:\n        validate_name_not_reserved_and_retry_once()  # else surface error\n    raise","preventionTips":["The route does NOT pre-check reserved names — the client must","Reuse one shared reserved-list constant across create and rename flows"],"tags":["auth","rename","reserved-username","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}