Merge branch 'main' into verify-ff065522ba55

This commit is contained in:
2026-09-15 00:35:40 +00:00
3 changed files with 128 additions and 4 deletions
+42 -1
View File
@@ -54,6 +54,12 @@ def _client_key(request: Request) -> str:
# brute-forcer.
_auth_limiter = SlidingWindowLimiter(max_hits=10, window_seconds=60)
# Every limiter in the process -- named limits built via `rate_limit()`
# below register themselves here too, so `reset_all()` (the test hook) and
# any future process-wide accounting stay correct without each caller
# having to remember to wire it up.
_registered_limiters: list[SlidingWindowLimiter] = [_auth_limiter]
async def auth_rate_limit(request: Request) -> None:
"""FastAPI dependency: rate-limit auth attempts per client IP.
@@ -68,6 +74,41 @@ async def auth_rate_limit(request: Request) -> None:
_auth_limiter.check(f"auth:{_client_key(request)}")
def rate_limit(name: str, *, max_hits: int, window_seconds: float):
"""Build a named, per-client-IP FastAPI rate-limit dependency.
Generalizes the ``auth_rate_limit`` pattern above for other endpoints
(e.g. chat send, message report) that want their own independent cap:
report_rate_limit = rate_limit("message_report", max_hits=5, window_seconds=60)
@router.post("/report", dependencies=[Depends(report_rate_limit)])
...
Each call gets its own ``SlidingWindowLimiter`` -- counters for one
named limit never bleed into another, even for the same client IP --
and reuses ``SlidingWindowLimiter.check`` for the actual sliding-window
accounting and the 429 + ``Retry-After`` response. Like
``auth_rate_limit``, the returned dependency is a no-op when
``settings.RATE_LIMIT_ENABLED`` is false, so the checkpoint smoke suite
(one client IP for the whole run) doesn't trip on it.
``name`` only needs to be unique within the process; it namespaces the
per-IP key so it doesn't collide with ``auth_rate_limit`` or any other
named limit.
"""
limiter = SlidingWindowLimiter(max_hits=max_hits, window_seconds=window_seconds)
_registered_limiters.append(limiter)
async def _dependency(request: Request) -> None:
if not settings.RATE_LIMIT_ENABLED:
return
limiter.check(f"{name}:{_client_key(request)}")
return _dependency
def reset_all() -> None:
"""Test hook."""
_auth_limiter.reset()
for limiter in _registered_limiters:
limiter.reset()