{"record":{"id":"96f666c2964812fd","repo":"langgenius/dify","slug":"email-send-ip-limit-96f666","errorCode":"email_send_ip_limit","errorMessage":"Too many emails have been sent from this IP address recently. Please try again later.","messagePattern":"Too many emails have been sent from this IP address recently\\. Please try again later\\.","errorType":"error_code","errorClass":"EmailSendIpLimitError","httpStatus":429,"severity":"warning","filePath":"api/controllers/console/auth/forgot_password.py","lineNumber":77,"sourceCode":"class ForgotPasswordSendEmailApi(Resource):\n    @console_ns.doc(\"send_forgot_password_email\")\n    @console_ns.doc(description=\"Send password reset email\")\n    @console_ns.expect(console_ns.models[ForgotPasswordSendPayload.__name__])\n    @console_ns.response(\n        200,\n        \"Email sent successfully\",\n        console_ns.models[ForgotPasswordEmailResponse.__name__],\n    )\n    @console_ns.response(400, \"Invalid email or rate limit exceeded\")\n    @setup_required\n    @email_password_login_enabled\n    @model_validate(ForgotPasswordSendPayload)\n    def post(self, req_data: ForgotPasswordSendPayload):\n        normalized_email = req_data.email.lower()\n\n        ip_address = extract_remote_ip(request)\n        if AccountService.is_email_send_ip_limit(ip_address):\n            raise EmailSendIpLimitError()\n\n        if req_data.language is not None and req_data.language == \"zh-Hans\":\n            language = \"zh-Hans\"\n        else:\n            language = \"en-US\"\n\n        account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())\n\n        token = AccountService.send_reset_password_email(\n            account=account,\n            email=normalized_email,\n            language=language,\n            is_allow_register=FeatureService.get_system_features().is_allow_register,\n        )\n\n        return {\"result\": \"success\", \"data\": token}\n\n","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/controllers/console/auth/forgot_password.py#L59-L95","documentation":"Raised by EmailSendIpLimitError in ForgotPasswordSendApi.post when AccountService.is_email_send_ip_limit(ip_address) returns true. The IP-based limiter caps how many password-reset emails can originate from one IP within a window, preventing email-bomb abuse of the reset endpoint.","triggerScenarios":"POST /console/api/forgot-password/email-send from an IP that has already triggered too many reset emails in the window. The check runs before the account is even looked up.","commonSituations":"Shared office/NAT IP with many users requesting resets; automated tests reusing one egress IP; a legitimate user clicking 'resend' rapidly; an attacker abusing the unauthenticated endpoint. VPN users all sharing one exit IP.","solutions":["Wait for the IP rate-limit window to expire before requesting another reset email.","Spread test traffic across multiple egress IPs or mock is_email_send_ip_limit in tests.","If the limit is too aggressive for a shared NAT, have an operator tune the rate-limit config for email-send IP caps.","Have the client debounce the 'send reset email' button and show the cooldown to the user."],"exampleFix":"// before\n<button onClick={() => sendReset(email)}>Resend</button>\n// after: debounce + cooldown\n<button disabled={!canResend} onClick={() => sendReset(email)}>Resend</button>","handlingStrategy":"retry","validationCode":"// Debounce resend and respect a local cooldown\nif (Date.now() - lastSentAt < 60_000) {\n  warnTooSoon();\n  return;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await sendReset({ email });\n} catch (e) {\n  if (e.code === 'email_send_ip_limit') showIpCooldown();\n  else throw e;\n}","preventionTips":["Debounce the 'send reset email' button with a visible cooldown.","For tests, spread traffic across egress IPs or mock is_email_send_ip_limit.","On shared NAT, tune the server-side IP cap to fit legitimate concurrency."],"tags":["auth","forgot-password","rate-limit","ip-based","abuse-protection"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}