odysseus-dev/odysseus · error · HTTPException

Username already taken

Error message

Username already taken

What it means

Raised as HTTP 409 by the POST /signup handler when auth_manager.create_user() returns falsy. The auth manager refuses to create a user whose (lowercased) username already exists in its user store, so the signup is rejected as a conflict. This is a business-rule conflict, not a server failure.

Source

Thrown at routes/auth_routes.py:135

    @router.post("/signup")
    async def signup(body: SignupRequest, request: Request):
        """Create a new user account. Only works if signup is enabled by admin."""
        if not _signup_limiter.check(request.client.host):
            raise HTTPException(429, "Too many requests — try again later")
        if not auth_manager.is_configured:
            raise HTTPException(400, "Run setup first")
        if not auth_manager.signup_enabled:
            raise HTTPException(403, "Registration is disabled. Ask an admin for an account.")
        if len(body.password) < PASSWORD_MIN_LENGTH:
            raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
        if len(body.username.strip()) < 1:
            raise HTTPException(400, "Username is required")
        if body.username.lower() in RESERVED_USERNAMES:
            raise HTTPException(403, "Username is reserved")
        ok = await asyncio.to_thread(auth_manager.create_user, body.username, body.password, is_admin=False)
        if not ok:
            raise HTTPException(409, "Username already taken")
        return {"ok": True, "message": "Account created"}

    @router.post("/login")
    async def login(body: LoginRequest, request: Request, response: Response):
        if not _login_limiter.check(request.client.host):
            raise HTTPException(429, "Too many requests — try again later")
        # Verify password first
        username = body.username.strip().lower()
        if not await asyncio.to_thread(auth_manager.verify_password, username, body.password):
            raise HTTPException(401, "Invalid credentials")
        # Check 2FA if enabled
        if auth_manager.totp_enabled(username):
            if not body.totp_code:
                # Password OK but need TOTP — tell client to show code input
                return {"ok": False, "requires_totp": True, "username": username}
            if not auth_manager.totp_verify(username, body.totp_code):
                raise HTTPException(401, "Invalid 2FA code")
        # All checks passed — create session (password already verified above)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Choose a different username and retry the signup request.
  2. If you already own the account, use POST /login instead of signing up again.
  3. If this was a double-submit, disable the submit button while the request is in flight.
  4. If you are an admin and must reuse the name, delete or rename the existing user via the admin /users endpoints first.

Example fix

// before
await fetch('/signup', {method:'POST', body: JSON.stringify({username:'alice', password:'...'})});
// after — handle the 409 conflict in the UI
const res = await fetch('/signup', {...});
if (res.status === 409) {
  setError('That username is taken — pick another.');
  return;
}
Defensive patterns

Strategy: validation

Validate before calling

async function signup(username, password) {
  // cheap pre-check: a taken name can be detected by attempting login? No —
  // simplest: just handle 409. But avoid double-submit:
  if (signup.inFlight) return;
  signup.inFlight = true;
  try {
    const res = await fetch('/signup', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({username, password})});
    if (res.status === 409) throw new Error('username-taken');
    return res.json();
  } finally { signup.inFlight = false; }
}

Try / catch

try { await signup(u, p); } catch (e) { if (e.message === 'username-taken') showFieldError('username', 'This name is taken'); else throw e; }

Prevention

When it happens

Trigger: Calling POST /signup with a username (case-insensitive) that already exists, after passing the earlier guards: setup configured, signup enabled, password >= PASSWORD_MIN_LENGTH, non-empty username, username not in RESERVED_USERNAMES.

Common situations: Double-submitting a signup form, retrying after a network timeout when the first request actually succeeded, picking a common username like 'admin' or 'test' that another user already claimed, or automated test suites re-running against a persistent user database.

Related errors


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