commit 5a845c01fc0d0405e11fd20dbbcfeaa7240c843e
parent a780d21a926a1f2bf7d0e47680ee5ff5f3d9d616
Author: claude-bot <noreply@anthropic.com>
Date: Mon, 5 Oct 2026 14:19:17 +0000
Rate-limit /api/shorten (10/min, 60/hour per IP) and cap URL length
Diffstat:
| M | app/main.py | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
1 file changed, 38 insertions(+), 0 deletions(-)
diff --git a/app/main.py b/app/main.py
@@ -255,6 +255,38 @@ class _UploadRateLimiter:
_upload_limiter = _UploadRateLimiter()
+
+
+class _CountRateLimiter:
+ """Sliding-window request counter per key (e.g. per client IP)."""
+
+ def __init__(self, per_minute: int, per_hour: int):
+ self.per_minute = per_minute
+ self.per_hour = per_hour
+ self._hits: dict[str, deque] = {}
+
+ def check_and_record(self, key: str):
+ now = time.time()
+ dq = self._hits.setdefault(key, deque())
+ while dq and now - dq[0] > 3600:
+ dq.popleft()
+ if sum(1 for t in dq if now - t <= 60) >= self.per_minute:
+ raise HTTPException(
+ status_code=429,
+ detail=f"Rate limit exceeded: {self.per_minute} requests/minute. Try again shortly.",
+ )
+ if len(dq) >= self.per_hour:
+ raise HTTPException(
+ status_code=429,
+ detail=f"Rate limit exceeded: {self.per_hour} requests/hour. Try again later.",
+ )
+ dq.append(now)
+ if len(self._hits) > 10000:
+ self._hits = {k: v for k, v in self._hits.items() if v and now - v[-1] <= 3600}
+
+
+_shorten_limiter = _CountRateLimiter(per_minute=10, per_hour=60)
+SHORTEN_MAX_URL_LENGTH = 2048
BATCH_MIN_FILES = 2
ALLOWED_UPLOAD_FOLDERS = {"uploads", "third-party"}
@@ -423,6 +455,12 @@ class ShortenRequest(BaseModel):
@app.post("/api/shorten")
async def api_shorten(request: Request, body: ShortenRequest):
url = body.url.strip()
+ if len(url) > SHORTEN_MAX_URL_LENGTH:
+ raise HTTPException(
+ status_code=400,
+ detail=f"URL too long (max {SHORTEN_MAX_URL_LENGTH} characters).",
+ )
+ _shorten_limiter.check_and_record(get_client_ip(request))
_assert_public_url(url)
short_id = await shorten_url(url)