odysseus-dev/odysseus · error · HTTPException

Username required

Error message

Username required

What it means

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.

Source

Thrown at routes/auth_routes.py:316

    async def update_user_privileges(username: str, request: Request):
        user = _get_current_user(request)
        if not user or not auth_manager.is_admin(user):
            raise HTTPException(403, "Admin only")
        body = await request.json()
        ok = auth_manager.set_privileges(username, body)
        if not ok:
            raise HTTPException(404, "User not found or is admin")
        return {"ok": True, "privileges": auth_manager.get_privileges(username)}

    @router.put("/users/{username}/rename")
    async def rename_user(username: str, body: RenameUserRequest, request: Request):
        user = _get_current_user(request)
        if not user or not auth_manager.is_admin(user):
            raise HTTPException(403, "Admin only")
        old_username = (username or "").strip().lower()
        new_username = (body.username or "").strip().lower()
        if not new_username:
            raise HTTPException(400, "Username required")
        if old_username == new_username:
            return {"ok": True, "username": new_username, "renamed_self": old_username == user}
        if old_username not in auth_manager.users:
            raise HTTPException(404, "User not found")
        if new_username in auth_manager.users:
            raise HTTPException(409, "Username already taken")

        # Gate on auth first. Every mutation below is contingent on this
        # succeeding — doing it last meant a rejected rename (e.g. reserved
        # username) left file-backed owner fields already rewritten with no
        # way to roll them back.
        ok = auth_manager.rename_user(old_username, new_username, user)
        if not ok:
            raise HTTPException(400, "Cannot rename user")

        def _rollback_auth_rename() -> bool:
            # On self-rename the admin session has already moved to the new
            # username, so the rollback must authenticate as the new user.

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Send a non-empty, non-whitespace username in the body field the RenameUserRequest model expects (username).
  2. Trim client-side before submit and disable the submit button for empty input.
  3. Confirm the request model field name against the OpenAPI schema (GET /openapi.json) if the field seems populated.

Example fix

// before
await api.put(`/users/${old}/rename`, { new_username: next });
// after
await api.put(`/users/${old}/rename`, { username: next.trim() });
Defensive patterns

Strategy: validation

Validate before calling

new_name = (payload.get('username') or '').strip()
if not new_name:
    raise ValueError('username field is required and must be non-empty')

Type guard

def is_valid_rename_body(body: dict) -> bool:
    return isinstance(body.get('username'), str) and bool(body['username'].strip())

Try / catch

try:
    put(f'/users/{old}/rename', {'username': new})
except HTTPError as e:
    if e.response.status_code == 400 and 'required' in e.response.json()['detail']:
        fix_payload_field()  # wrong/empty field — never retry unchanged
    raise

Prevention

When it happens

Trigger: POSTing {"username": ""} or {"username": " "} in the JSON body; client sending {"new_username": ...} (wrong field name) so the expected field deserializes as empty/None.

Common situations: Form submitted with an empty rename field; mismatch between API docs and client model (new_username vs username); whitespace-only input from copy-paste.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/17907620b6514b2b. Report an issue: GitHub.