{"record":{"id":"17907620b6514b2b","repo":"odysseus-dev/odysseus","slug":"username-required","errorCode":null,"errorMessage":"Username required","messagePattern":"Username required","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":316,"sourceCode":"    async def update_user_privileges(username: str, request: Request):\n        user = _get_current_user(request)\n        if not user or not auth_manager.is_admin(user):\n            raise HTTPException(403, \"Admin only\")\n        body = await request.json()\n        ok = auth_manager.set_privileges(username, body)\n        if not ok:\n            raise HTTPException(404, \"User not found or is admin\")\n        return {\"ok\": True, \"privileges\": auth_manager.get_privileges(username)}\n\n    @router.put(\"/users/{username}/rename\")\n    async def rename_user(username: str, body: RenameUserRequest, request: Request):\n        user = _get_current_user(request)\n        if not user or not auth_manager.is_admin(user):\n            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.","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L298-L334","documentation":"Raised by PUT /users/{username}/rename when body.username (RenameUserRequest) is empty after strip().lower(). The path username is not validated for emptiness here — only the NEW name is, because the 'old not in users' check below covers a bogus old name with 404.","triggerScenarios":"POSTing {\"username\": \"\"} or {\"username\": \"   \"} in the JSON body; client sending {\"new_username\": ...} (wrong field name) so the expected field deserializes as empty/None.","commonSituations":"Form submitted with an empty rename field; mismatch between API docs and client model (new_username vs username); whitespace-only input from copy-paste.","solutions":["Send a non-empty, non-whitespace username in the body field the RenameUserRequest model expects (username).","Trim client-side before submit and disable the submit button for empty input.","Confirm the request model field name against the OpenAPI schema (GET /openapi.json) if the field seems populated."],"exampleFix":"// before\nawait api.put(`/users/${old}/rename`, { new_username: next });\n// after\nawait api.put(`/users/${old}/rename`, { username: next.trim() });","handlingStrategy":"validation","validationCode":"new_name = (payload.get('username') or '').strip()\nif not new_name:\n    raise ValueError('username field is required and must be non-empty')","typeGuard":"def is_valid_rename_body(body: dict) -> bool:\n    return isinstance(body.get('username'), str) and bool(body['username'].strip())","tryCatchPattern":"try:\n    put(f'/users/{old}/rename', {'username': new})\nexcept HTTPError as e:\n    if e.response.status_code == 400 and 'required' in e.response.json()['detail']:\n        fix_payload_field()  # wrong/empty field — never retry unchanged\n    raise","preventionTips":["Trim input and block empty submits client-side","Confirm the body field name against the OpenAPI schema"],"tags":["auth","validation","rename","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}