Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions slowapi/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,22 @@ class HEADERS:
MAX_BACKEND_CHECKS = 5


def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> Response:
def _rate_limit_exceeded_handler(request: Request, exc: Exception) -> Response:
"""
Build a simple JSON response that includes the details of the rate limit
that was hit. If no limit is hit, the countdown is added to headers.

Handles exceptions that may not have a 'detail' attribute (e.g., ConnectionError).
"""
# Safely get detail attribute, fallback to exception message if not available
if isinstance(exc, RateLimitExceeded):
detail = getattr(exc, 'detail', "Rate limit exceeded")
else:
# For other exceptions (e.g., ConnectionError), use exception message
detail = str(exc)

response = JSONResponse(
{"error": f"Rate limit exceeded: {exc.detail}"}, status_code=429
{"error": f"Rate limit exceeded: {detail}"}, status_code=429
)
response = request.app.state.limiter._inject_headers(
response, request.state.view_rate_limit
Expand Down
52 changes: 52 additions & 0 deletions tests/test_fastapi_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from starlette.testclient import TestClient

from slowapi.util import get_ipaddr
from slowapi.extension import _rate_limit_exceeded_handler
from tests import TestSlowapi


Expand Down Expand Up @@ -369,3 +370,54 @@ async def t1_func(my_param: str, request: Request):
)
== 2
)

def test_rate_limit_exceeded_handler_with_detail(self, build_fastapi_app):
"""Test that RateLimitExceeded exceptions with detail attribute work correctly."""

app, limiter = build_fastapi_app(key_func=get_ipaddr)

@app.get("/test")
@limiter.limit("1/minute")
async def test_endpoint(request: Request):
return {"message": "test"}

client = TestClient(app)

# Make first request - should succeed
response1 = client.get("/test")
assert response1.status_code == 200

# Make second request - should be rate limited
response2 = client.get("/test")
assert response2.status_code == 429
assert "error" in response2.json()
assert "Rate limit exceeded" in response2.json()["error"]

def test_rate_limit_exceeded_handler_without_detail(self, build_fastapi_app):
"""Test that exceptions without 'detail' attribute are handled gracefully.

This tests the fix for issue #213 where ConnectionError or other exceptions
without a 'detail' attribute would cause AttributeError.
"""
app, limiter = build_fastapi_app(key_func=get_ipaddr)
client = TestClient(app)

# Create an exception without a 'detail' attribute (simulating ConnectionError)
class ExceptionWithoutDetail(Exception):
def __init__(self):
super().__init__("Connection failed")

@app.get("/test")
async def test_endpoint(request: Request):
# Manually trigger the handler with an exception without detail
# This simulates the bug scenario from issue #213
exc = ExceptionWithoutDetail()
response = _rate_limit_exceeded_handler(request, exc)
return response

# Should not crash with AttributeError
response = client.get("/test")
assert response.status_code == 429
assert "error" in response.json()
assert "Rate limit exceeded" in response.json()["error"]
assert "Connection failed" in response.json()["error"]