{"record":{"id":"eb97b845d0d84096","repo":"bytedance/deer-flow","slug":"too-many-login-attempts-try-again-later","errorCode":null,"errorMessage":"Too many login attempts. Try again later.","messagePattern":"Too many login attempts\\. Try again later\\.","errorType":"http","errorClass":"HTTPException","httpStatus":429,"severity":"warning","filePath":"backend/app/gateway/routers/auth.py","lineNumber":246,"sourceCode":"                real_ip = request.headers.get(\"x-real-ip\", \"\").strip()\n                if real_ip:\n                    return real_ip\n        except ValueError:\n            # peer_host wasn't a parseable IP (e.g. \"unknown\") — fall through\n            pass\n\n    return peer_host or \"unknown\"\n\n\ndef _check_rate_limit(ip: str) -> None:\n    \"\"\"Raise 429 if the IP is currently locked out.\"\"\"\n    record = _login_attempts.get(ip)\n    if record is None:\n        return\n    fail_count, lock_until = record\n    if fail_count >= _MAX_LOGIN_ATTEMPTS:\n        if time.time() < lock_until:\n            raise HTTPException(\n                status_code=429,\n                detail=\"Too many login attempts. Try again later.\",\n            )\n        del _login_attempts[ip]\n\n\n_MAX_TRACKED_IPS = 10000\n\n\ndef _record_login_failure(ip: str) -> None:\n    \"\"\"Record a failed login attempt for the given IP.\"\"\"\n    # Evict expired lockouts when dict grows too large\n    if len(_login_attempts) >= _MAX_TRACKED_IPS:\n        now = time.time()\n        expired = [k for k, (c, t) in _login_attempts.items() if c >= _MAX_LOGIN_ATTEMPTS and now >= t]\n        for k in expired:\n            del _login_attempts[k]\n        # If still too large, evict cheapest-to-lose half: below-threshold","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/auth.py#L228-L264","documentation":"429 from the login route's in-process rate limiter: after _MAX_LOGIN_ATTEMPTS consecutive failures for a client IP, a lockout window is set and every further attempt during that window raises immediately, before credential verification. The lockout clears when the window expires (the record is deleted on the next post-lockout check).","triggerScenarios":"Repeated failed POST /api/auth/login from the same client IP (as derived by _get_client_ip, e.g. X-Forwarded-For or peer address) within the lockout window; also triggered by CI test suites hammering login with bad credentials.","commonSituations":"Automated retries with a wrong password; tests running many login failures without unique IPs; all users behind one NAT/proxy sharing a single client IP so one bad actor locks everyone out; stale X-Forwarded-For trust making the IP key wrong.","solutions":["Stop retrying and wait for the lockout window to elapse, then retry with verified credentials","Fix the underlying credential failure (reset password) before the next attempt","In tests, reset state by restarting the Gateway or use distinct client IPs / successful logins to clear records","Ensure the proxy in front of the Gateway forwards the correct client IP so lockouts are per-user, not global"],"exampleFix":"# before\nfor pw in candidates:\n    try_login(email, pw)  # hammers endpoint, triggers 429\n\n# after\nresp = try_login(email, first_candidate)\nif resp.status_code == 429:\n    backoff(resp.retry_after or 60)  # wait out lockout before next attempt","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await login(email, pw); } catch (e) {\n  if (e.status === 429) { notify('Locked out; wait before retrying'); scheduleRetry(afterLockoutWindow()); return; }\n  throw e;\n}","preventionTips":["Never loop login attempts without backoff; cap retries below _MAX_LOGIN_ATTEMPTS","In tests, restart the Gateway between failed-login batches or use unique client IPs","Fix the proxy chain so the Gateway sees true client IPs, avoiding shared lockouts"],"tags":["auth","http-429","rate-limit","login"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}