fix(rate-limit): stop double-counting requests in middleware
Multiple RateLimitMiddleware instances were each calling check_allowed() on every request, halving the effective global limit (200 req/min became ~100). Added path_prefixes and skip_paths so each instance only checks the paths it owns. - Auth middleware scoped to /api/v1/auth/login and /api/v1/setup - History middleware scoped to /api/v1/history - Global middleware skips auth and history paths - Updated tests to match single-count behavior
This commit is contained in:
@@ -134,24 +134,17 @@ class TestRateLimitMiddleware:
|
||||
"""Global rate limit should block requests exceeding per-IP limit."""
|
||||
await _do_setup(client)
|
||||
|
||||
# Create a client that mimics a specific IP
|
||||
# We'll make many requests and see if we hit the limit
|
||||
limiter = client._transport.app.state.global_rate_limiter
|
||||
limiter.reset()
|
||||
|
||||
# Reduce limit temporarily for testing.
|
||||
# Each request is checked by two middleware instances, so the
|
||||
# effective limit is doubled for non-bucket endpoints.
|
||||
original_max = limiter.max_requests
|
||||
limiter.max_requests = 7
|
||||
limiter.max_requests = 3
|
||||
|
||||
try:
|
||||
# First 3 requests should succeed
|
||||
for i in range(3):
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 200, f"Request {i + 1} failed"
|
||||
|
||||
# Fourth request should be rate limited
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 429
|
||||
assert response.json()["code"] == "rate_limit_exceeded"
|
||||
@@ -166,22 +159,47 @@ class TestRateLimitMiddleware:
|
||||
limiter = client._transport.app.state.global_rate_limiter
|
||||
limiter.reset()
|
||||
|
||||
# Two middleware instances check each request, so the effective
|
||||
# limit is doubled for non-bucket endpoints.
|
||||
original_max = limiter.max_requests
|
||||
limiter.max_requests = 3
|
||||
limiter.max_requests = 2
|
||||
|
||||
try:
|
||||
# First request succeeds
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Second request is rate limited
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 429
|
||||
assert "Retry-After" in response.headers
|
||||
retry_after = int(response.headers["Retry-After"])
|
||||
assert retry_after > 0
|
||||
assert retry_after <= 60 # Should be less than window
|
||||
assert retry_after <= 60
|
||||
finally:
|
||||
limiter.max_requests = original_max
|
||||
|
||||
async def test_auth_bucket_allows_more_requests(self, client: AsyncClient) -> None:
|
||||
"""Auth endpoints use a dedicated high-rate bucket."""
|
||||
await _do_setup(client)
|
||||
|
||||
limiter = client._transport.app.state.global_rate_limiter
|
||||
limiter.reset()
|
||||
|
||||
# The auth bucket is configured for 1000 req/min; we only need to
|
||||
# verify that it is *not* the global bucket (200 req/min).
|
||||
for _ in range(5):
|
||||
response = await client.post("/api/v1/auth/login", json={"password": "x"})
|
||||
assert response.status_code in (401, 403, 429)
|
||||
|
||||
async def test_history_bucket_allows_more_requests(self, client: AsyncClient) -> None:
|
||||
"""History endpoints use a dedicated high-rate bucket."""
|
||||
await _do_setup(client)
|
||||
|
||||
limiter = client._transport.app.state.global_rate_limiter
|
||||
limiter.reset()
|
||||
|
||||
for _ in range(5):
|
||||
response = await client.get("/api/v1/history/bans")
|
||||
# 401/403 is fine — we just need to confirm we are not 429'd
|
||||
# by the global limiter.
|
||||
assert response.status_code != 429
|
||||
|
||||
Reference in New Issue
Block a user