rate_limit.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. from __future__ import annotations
  2. from fastapi import Request, Response
  3. from starlette.responses import JSONResponse
  4. from core_shared.rate_limit import RateLimitDecision, RateLimiter
  5. from app.bootstrap.settings import ApiGatewaySettings
  6. RATE_LIMIT_WINDOW_SECONDS = 60
  7. RATE_LIMIT_LIMIT_HEADER = "x-ratelimit-limit"
  8. RATE_LIMIT_REMAINING_HEADER = "x-ratelimit-remaining"
  9. RATE_LIMIT_RESET_HEADER = "x-ratelimit-reset"
  10. def enforce_gateway_rate_limit(
  11. *,
  12. request: Request,
  13. settings: ApiGatewaySettings,
  14. limiter: RateLimiter,
  15. ) -> Response | None:
  16. if not settings.rate_limit_enabled:
  17. return None
  18. if not request.url.path.startswith("/gateway/"):
  19. return None
  20. from app.infrastructure.request_context import get_gateway_request_context
  21. context = get_gateway_request_context(request)
  22. checks: list[RateLimitDecision] = []
  23. tenant_limit = max(settings.tenant_rate_limit_per_minute, 1)
  24. checks.append(
  25. limiter.check(
  26. key=f"tenant:{context.tenant_id}",
  27. limit=tenant_limit,
  28. window_seconds=RATE_LIMIT_WINDOW_SECONDS,
  29. )
  30. )
  31. if context.api_key_id is not None:
  32. api_key_limit = max(settings.api_key_rate_limit_per_minute, 1)
  33. checks.append(
  34. limiter.check(
  35. key=f"api-key:{context.api_key_id}",
  36. limit=api_key_limit,
  37. window_seconds=RATE_LIMIT_WINDOW_SECONDS,
  38. )
  39. )
  40. denied = next((item for item in checks if not item.allowed), None)
  41. if denied is None:
  42. if checks:
  43. request.state.gateway_rate_limit_decision = min(
  44. checks,
  45. key=lambda item: item.remaining,
  46. )
  47. return None
  48. response = JSONResponse(
  49. status_code=429,
  50. content={
  51. "detail": "rate limit exceeded",
  52. "limit": denied.limit,
  53. "reset_epoch_seconds": denied.reset_epoch_seconds,
  54. },
  55. )
  56. apply_rate_limit_headers(response, denied)
  57. return response
  58. def apply_gateway_rate_limit_headers(
  59. *,
  60. response: Response,
  61. request: Request,
  62. settings: ApiGatewaySettings,
  63. limiter: RateLimiter,
  64. ) -> None:
  65. if not settings.rate_limit_enabled:
  66. return
  67. if not request.url.path.startswith("/gateway/"):
  68. return
  69. decision = getattr(request.state, "gateway_rate_limit_decision", None)
  70. if isinstance(decision, RateLimitDecision):
  71. apply_rate_limit_headers(response, decision)
  72. def apply_rate_limit_headers(response: Response, decision: RateLimitDecision) -> None:
  73. response.headers[RATE_LIMIT_LIMIT_HEADER] = str(decision.limit)
  74. response.headers[RATE_LIMIT_REMAINING_HEADER] = str(decision.remaining)
  75. response.headers[RATE_LIMIT_RESET_HEADER] = str(decision.reset_epoch_seconds)