Skip to content

Commit 90a9c55

Browse files
committed
fix(backend): require Request for rate limiting, enable proxy headers
- rate_limit decorator now requires Request parameter (raises 500 if missing) - Routes updated: request: Request, body: PydanticModel pattern - Added proxy-headers flag to Dockerfile for X-Forwarded-For support - Added FORWARDED_ALLOW_IPS env var to docker-compose - Documented proxy header configuration in decorator docstring Behind reverse proxy, this ensures rate limiting uses real client IP instead of proxy IP (which would bucket all users together).
1 parent 69b9c5f commit 90a9c55

4 files changed

Lines changed: 31 additions & 19 deletions

File tree

backend/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ EXPOSE 8000
2929
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
3030
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()" || exit 1
3131

32-
# Run with uvicorn
33-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
32+
# Run with uvicorn (proxy-headers enabled for rate limiting behind reverse proxy)
33+
# Set FORWARDED_ALLOW_IPS env var in production to your proxy's IP range
34+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

backend/routes/feedback.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import httpx
77
from datetime import datetime
8-
from fastapi import APIRouter, HTTPException
8+
from fastapi import APIRouter, HTTPException, Request
99
from pydantic import BaseModel, EmailStr
1010
from typing import Optional
1111
from services.rate_limiter import rate_limit
@@ -52,12 +52,12 @@ async def post_to_discord(embed: dict) -> bool:
5252

5353
@router.post("")
5454
@rate_limit(requests_per_minute=5)
55-
async def submit_feedback(request: FeedbackRequest):
55+
async def submit_feedback(request: Request, body: FeedbackRequest):
5656
"""Submit user feedback - posts to Discord."""
5757
if not DISCORD_WEBHOOK_URL:
5858
raise HTTPException(status_code=503, detail="Feedback service unavailable")
5959

60-
mood_info = MOOD_CONFIG.get(request.mood, MOOD_CONFIG["good"])
60+
mood_info = MOOD_CONFIG.get(body.mood, MOOD_CONFIG["good"])
6161

6262
embed = {
6363
"title": "💬 New Feedback",
@@ -69,11 +69,11 @@ async def submit_feedback(request: FeedbackRequest):
6969
"timestamp": datetime.utcnow().isoformat(),
7070
}
7171

72-
if request.email:
73-
embed["fields"].append({"name": "User", "value": request.email, "inline": True})
72+
if body.email:
73+
embed["fields"].append({"name": "User", "value": body.email, "inline": True})
7474

75-
if request.message:
76-
embed["fields"].append({"name": "Message", "value": request.message[:1000], "inline": False})
75+
if body.message:
76+
embed["fields"].append({"name": "Message", "value": body.message[:1000], "inline": False})
7777

7878
success = await post_to_discord(embed)
7979
if not success:
@@ -84,19 +84,19 @@ async def submit_feedback(request: FeedbackRequest):
8484

8585
@router.post("/waitlist")
8686
@rate_limit(requests_per_minute=3)
87-
async def join_waitlist(request: WaitlistRequest):
87+
async def join_waitlist(request: Request, body: WaitlistRequest):
8888
"""Join waitlist for Pro or Enterprise plan."""
8989
if not DISCORD_WEBHOOK_URL:
9090
raise HTTPException(status_code=503, detail="Waitlist service unavailable")
9191

92-
is_enterprise = request.plan.lower() == "enterprise"
92+
is_enterprise = body.plan.lower() == "enterprise"
9393

9494
if is_enterprise:
9595
embed = {
9696
"title": "🏢 Enterprise Inquiry",
9797
"color": 0x8B5CF6,
9898
"fields": [
99-
{"name": "Email", "value": request.email, "inline": True},
99+
{"name": "Email", "value": body.email, "inline": True},
100100
{"name": "Plan", "value": "Enterprise (Custom)", "inline": True},
101101
],
102102
"footer": {"text": "OpenCodeIntel Enterprise"},
@@ -107,7 +107,7 @@ async def join_waitlist(request: WaitlistRequest):
107107
"title": "🚀 New Waitlist Signup",
108108
"color": 0x3B82F6,
109109
"fields": [
110-
{"name": "Email", "value": request.email, "inline": True},
110+
{"name": "Email", "value": body.email, "inline": True},
111111
{"name": "Plan Interest", "value": "Pro ($19/month)", "inline": True},
112112
],
113113
"footer": {"text": "OpenCodeIntel Waitlist"},

backend/services/rate_limiter.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,33 @@ def rate_limit(requests_per_minute: int = 60):
2323
Simple rate limit decorator for FastAPI routes.
2424
Uses in-memory storage - suitable for single-instance deployments.
2525
For production, use Redis-backed RateLimiter class instead.
26+
27+
IMPORTANT: Routes using this decorator MUST include `request: Request` as
28+
a parameter. For correct client IP detection behind a reverse proxy,
29+
configure Uvicorn with:
30+
--proxy-headers --forwarded-allow-ips="<your-proxy-ips>"
31+
This ensures request.client.host reflects the real client IP from
32+
X-Forwarded-For header, not the proxy's IP.
2633
"""
2734
def decorator(func: Callable):
2835
@wraps(func)
2936
async def wrapper(*args, **kwargs):
30-
# Try to get request from kwargs or args
37+
# Get Request from kwargs (FastAPI injects it by parameter name)
3138
request = kwargs.get('request')
3239
if not request:
3340
for arg in args:
3441
if isinstance(arg, Request):
3542
request = arg
3643
break
3744

38-
# Get client identifier (IP or fallback)
39-
if request:
40-
client_id = request.client.host if request.client else "unknown"
41-
else:
42-
client_id = "unknown"
45+
if not request or not isinstance(request, Request):
46+
raise HTTPException(
47+
status_code=500,
48+
detail="Rate limiting requires Request parameter in route"
49+
)
50+
51+
# Get client IP (relies on Uvicorn proxy-headers config for real IP)
52+
client_id = request.client.host if request.client else "unknown"
4353

4454
key = f"{func.__name__}:{client_id}"
4555
now = time.time()

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ services:
3737
- API_KEY=${API_KEY}
3838
- BACKEND_API_URL=http://backend:8000
3939
- DISCORD_FEEDBACK_WEBHOOK=${DISCORD_FEEDBACK_WEBHOOK}
40+
- FORWARDED_ALLOW_IPS=*
4041
volumes:
4142
- ./backend/repos:/app/repos
4243
- ./backend:/app

0 commit comments

Comments
 (0)