diff --git a/calendarium/api.py b/calendarium/api.py index 6c6ca96..976ecd1 100644 --- a/calendarium/api.py +++ b/calendarium/api.py @@ -15,6 +15,7 @@ from ninja.decorators import decorate_view from ninja.renderers import JSONRenderer from ninja.responses import NinjaJSONEncoder +from ninja.throttling import AnonRateThrottle from pydantic import AnyUrl, AnyHttpUrl, conint, constr, validator from . import datetools, liturgics, views @@ -25,6 +26,27 @@ BURST_RATE = settings.ORTHOCAL_API_RATELIMIT +class ShadowAnonRateThrottle(AnonRateThrottle): + """ + Runs the real per-IP rate check and logs what WOULD be throttled, but + always allows the request through -- no client sees a 429 yet. This is + a deliberate rollout step: BURST_RATE was defined but never wired up to + anything, so there's no data on how real (including third-party, non- + browser) traffic would be affected by actually enforcing it. Watch the + logs this produces for a while, then swap this for AnonRateThrottle + directly (same rate, same identity-by-IP behavior) once it looks safe. + """ + def allow_request(self, request): + allowed = super().allow_request(request) + if not allowed: + logger.warning( + 'Would rate-limit (shadow mode, not enforced): ip=%s path=%s ua=%s', + self.get_ident(request), + request.path, + request.META.get('HTTP_USER_AGENT', ''), + ) + return True + class Encoder(NinjaJSONEncoder): def default(self, o): if isinstance(o, AnyUrl): @@ -59,7 +81,8 @@ def get_openapi_operation_id(self, operation): ), servers=[ {'url': settings.ORTHOCAL_PUBLIC_URL, 'description': 'Public API'}, - ] + ], + throttle=[ShadowAnonRateThrottle(BURST_RATE)], )