feat(throttling): add auth throttling and structured cooldown errors

This commit is contained in:
2026-04-30 15:25:45 +03:30
parent 3152284cf3
commit 08e1793765
5 changed files with 338 additions and 1 deletions

View File

@@ -1,11 +1,14 @@
import logging
import traceback
from collections.abc import Iterable
from datetime import timedelta
from typing import Any
from django.conf import settings
from django.utils import timezone
from rest_framework import status as http_status
from rest_framework.exceptions import ErrorDetail
from rest_framework.exceptions import Throttled
from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler
@@ -83,7 +86,36 @@ def exception_handler(exc, context) -> Response:
if status_code < 500:
messages = _to_str_list(detail)
payload = _format_payload(messages, status_code)
return Response(payload, status=status_code)
if isinstance(exc, Throttled):
request = context.get("request")
wait = getattr(exc, "wait", None)
retry_after_seconds = None
if wait is not None:
retry_after_seconds = max(int(wait), 0)
elif request is not None:
retry_after_seconds = getattr(request, "_retry_after_seconds", None)
throttle_scope = getattr(request, "_throttle_scope", None) if request else None
payload.update(
{
"code": "throttled",
"scope": throttle_scope,
"retry_after_seconds": retry_after_seconds,
"throttled_until": (
timezone.now() + timedelta(seconds=retry_after_seconds)
).isoformat()
if retry_after_seconds is not None
else None,
}
)
formatted_response = Response(payload, status=status_code)
for header, value in response.headers.items():
formatted_response[header] = value
if isinstance(exc, Throttled) and "Retry-After" not in formatted_response:
request = context.get("request")
retry_after_seconds = getattr(request, "_retry_after_seconds", None) if request else None
if retry_after_seconds is not None:
formatted_response["Retry-After"] = str(max(int(retry_after_seconds), 0))
return formatted_response
traceback_text = traceback.format_exc()
payload = _format_payload(