rate_limit: Show html page when rate limited at /new/ endpoint.

Previously this showed a json error, but this is an endpoint that human
users use in the browser, so a proper HTML page is more appropriate.
This commit is contained in:
Mateusz Mandera 2021-07-25 17:07:06 +02:00 committed by Tim Abbott
parent 3ec55e7976
commit 4161e0caeb
3 changed files with 59 additions and 11 deletions

View File

@ -0,0 +1,23 @@
{% extends "zerver/portico.html" %}
{% block portico_content %}
<div class="error_page">
<div class="container">
<div class="row-fluid">
<img src="/static/images/500art.svg" alt=""/>
<div class="errorbox">
<div class="errorcontent">
<h1 class="lead">{{ _("Rate limit exceeded.") }}</h1>
<p>
{% trans %}You have exceeded the limit for how
often a user can perform this action.{% endtrans %}
{% trans %}You can try again in {{retry_after}} seconds.{% endtrans %}
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
import time import time
from typing import Callable from typing import Callable, Optional
from unittest import mock, skipUnless from unittest import mock, skipUnless
import DNS import DNS
@ -134,19 +134,29 @@ class RateLimitTests(ZulipTestCase):
newlimit = int(result["X-RateLimit-Remaining"]) newlimit = int(result["X-RateLimit-Remaining"])
self.assertEqual(limit, newlimit + 1) self.assertEqual(limit, newlimit + 1)
def do_test_hit_ratelimits(self, request_func: Callable[[], HttpResponse]) -> HttpResponse: def do_test_hit_ratelimits(
self,
request_func: Callable[[], HttpResponse],
assert_func: Optional[Callable[[HttpResponse], None]] = None,
) -> HttpResponse:
def default_assert_func(result: HttpResponse) -> None:
self.assertEqual(result.status_code, 429)
json = result.json()
self.assertEqual(json.get("result"), "error")
self.assertIn("API usage exceeded rate limit", json.get("msg"))
self.assertEqual(json.get("retry-after"), 0.5)
self.assertTrue("Retry-After" in result)
self.assertEqual(result["Retry-After"], "0.5")
if assert_func is None:
assert_func = default_assert_func
start_time = time.time() start_time = time.time()
for i in range(6): for i in range(6):
with mock.patch("time.time", return_value=(start_time + i * 0.1)): with mock.patch("time.time", return_value=(start_time + i * 0.1)):
result = request_func() result = request_func()
self.assertEqual(result.status_code, 429) assert_func(result)
json = result.json()
self.assertEqual(json.get("result"), "error")
self.assertIn("API usage exceeded rate limit", json.get("msg"))
self.assertEqual(json.get("retry-after"), 0.5)
self.assertTrue("Retry-After" in result)
self.assertEqual(result["Retry-After"], "0.5")
# We simulate waiting a second here, rather than force-clearing our history, # We simulate waiting a second here, rather than force-clearing our history,
# to make sure the rate-limiting code automatically forgives a user # to make sure the rate-limiting code automatically forgives a user
@ -173,12 +183,17 @@ class RateLimitTests(ZulipTestCase):
remove_ratelimit_rule(1, 5, domain="api_by_ip") remove_ratelimit_rule(1, 5, domain="api_by_ip")
def test_create_realm_rate_limiting(self) -> None: def test_create_realm_rate_limiting(self) -> None:
def assert_func(result: HttpResponse) -> None:
self.assertEqual(result.status_code, 429)
self.assert_in_response("Rate limit exceeded.", result)
with self.settings(OPEN_REALM_CREATION=True): with self.settings(OPEN_REALM_CREATION=True):
add_ratelimit_rule(1, 5, domain="create_realm_by_ip") add_ratelimit_rule(1, 5, domain="create_realm_by_ip")
try: try:
RateLimitedIPAddr("127.0.0.1").clear_history() RateLimitedIPAddr("127.0.0.1").clear_history()
self.do_test_hit_ratelimits( self.do_test_hit_ratelimits(
lambda: self.client_post("/new/", {"email": "new@zulip.com"}) lambda: self.client_post("/new/", {"email": "new@zulip.com"}),
assert_func=assert_func,
) )
finally: finally:
remove_ratelimit_rule(1, 5, domain="create_realm_by_ip") remove_ratelimit_rule(1, 5, domain="create_realm_by_ip")

View File

@ -45,6 +45,7 @@ from zerver.lib.actions import (
lookup_default_stream_groups, lookup_default_stream_groups,
) )
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
from zerver.lib.exceptions import RateLimited
from zerver.lib.onboarding import send_initial_realm_messages, setup_realm_internal_bots from zerver.lib.onboarding import send_initial_realm_messages, setup_realm_internal_bots
from zerver.lib.pysa import mark_sanitized from zerver.lib.pysa import mark_sanitized
from zerver.lib.send_email import EmailNotDeliveredException, FromAddress, send_email from zerver.lib.send_email import EmailNotDeliveredException, FromAddress, send_email
@ -590,7 +591,16 @@ def create_realm(request: HttpRequest, creation_key: Optional[str] = None) -> Ht
if request.method == "POST": if request.method == "POST":
form = RealmCreationForm(request.POST) form = RealmCreationForm(request.POST)
if form.is_valid(): if form.is_valid():
rate_limit_request_by_ip(request, domain="create_realm_by_ip") try:
rate_limit_request_by_ip(request, domain="create_realm_by_ip")
except RateLimited as e:
assert e.secs_to_freedom is not None
return render(
request,
"zerver/rate_limit_exceeded.html",
context={"retry_after": int(e.secs_to_freedom)},
status=429,
)
email = form.cleaned_data["email"] email = form.cleaned_data["email"]
activation_url = prepare_activation_url(email, request, realm_creation=True) activation_url = prepare_activation_url(email, request, realm_creation=True)