{"record":{"id":"3e49fddfbb4dbda6","repo":"odysseus-dev/odysseus","slug":"username-already-taken","errorCode":null,"errorMessage":"Username already taken","messagePattern":"Username already taken","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":135,"sourceCode":"\n    @router.post(\"/signup\")\n    async def signup(body: SignupRequest, request: Request):\n        \"\"\"Create a new user account. Only works if signup is enabled by admin.\"\"\"\n        if not _signup_limiter.check(request.client.host):\n            raise HTTPException(429, \"Too many requests — try again later\")\n        if not auth_manager.is_configured:\n            raise HTTPException(400, \"Run setup first\")\n        if not auth_manager.signup_enabled:\n            raise HTTPException(403, \"Registration is disabled. Ask an admin for an account.\")\n        if len(body.password) < PASSWORD_MIN_LENGTH:\n            raise HTTPException(400, f\"Password must be at least {PASSWORD_MIN_LENGTH} characters\")\n        if len(body.username.strip()) < 1:\n            raise HTTPException(400, \"Username is required\")\n        if body.username.lower() in RESERVED_USERNAMES:\n            raise HTTPException(403, \"Username is reserved\")\n        ok = await asyncio.to_thread(auth_manager.create_user, body.username, body.password, is_admin=False)\n        if not ok:\n            raise HTTPException(409, \"Username already taken\")\n        return {\"ok\": True, \"message\": \"Account created\"}\n\n    @router.post(\"/login\")\n    async def login(body: LoginRequest, request: Request, response: Response):\n        if not _login_limiter.check(request.client.host):\n            raise HTTPException(429, \"Too many requests — try again later\")\n        # Verify password first\n        username = body.username.strip().lower()\n        if not await asyncio.to_thread(auth_manager.verify_password, username, body.password):\n            raise HTTPException(401, \"Invalid credentials\")\n        # Check 2FA if enabled\n        if auth_manager.totp_enabled(username):\n            if not body.totp_code:\n                # Password OK but need TOTP — tell client to show code input\n                return {\"ok\": False, \"requires_totp\": True, \"username\": username}\n            if not auth_manager.totp_verify(username, body.totp_code):\n                raise HTTPException(401, \"Invalid 2FA code\")\n        # All checks passed — create session (password already verified above)","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L117-L153","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Choose a different username and retry the signup request.","If you already own the account, use POST /login instead of signing up again.","If this was a double-submit, disable the submit button while the request is in flight.","If you are an admin and must reuse the name, delete or rename the existing user via the admin /users endpoints first."],"exampleFix":"// before\nawait fetch('/signup', {method:'POST', body: JSON.stringify({username:'alice', password:'...'})});\n// after — handle the 409 conflict in the UI\nconst res = await fetch('/signup', {...});\nif (res.status === 409) {\n  setError('That username is taken — pick another.');\n  return;\n}","handlingStrategy":"validation","validationCode":"async function signup(username, password) {\n  // cheap pre-check: a taken name can be detected by attempting login? No —\n  // simplest: just handle 409. But avoid double-submit:\n  if (signup.inFlight) return;\n  signup.inFlight = true;\n  try {\n    const res = await fetch('/signup', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({username, password})});\n    if (res.status === 409) throw new Error('username-taken');\n    return res.json();\n  } finally { signup.inFlight = false; }\n}","typeGuard":null,"tryCatchPattern":"try { await signup(u, p); } catch (e) { if (e.message === 'username-taken') showFieldError('username', 'This name is taken'); else throw e; }","preventionTips":["Disable the submit button while the signup request is in flight to prevent duplicate creates.","Suggest alternative usernames in the UI when a 409 comes back.","Treat 409 on retry-after-timeout as 'may have succeeded' — check via login before re-submitting."],"tags":["auth","http-409","conflict","signup","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}