* Stop multi-file uploads from tripping the per-IP concurrency guard The /api/upload concurrency check summed its condition over `files`, but the condition didn't reference the loop variable — so it collapsed to len(files) whenever the IP had any recent upload. A single multi-file batch sent right after another upload therefore counted itself as N concurrent uploads and hit max_concurrent_uploads (3), returning 429. The browser swallows the 429 (no `files` in the body) and sends the chat with no attachments, so the model "doesn't even see" them (issue #1346). Count genuine recent upload events instead, via a pure count_recent_uploads() helper, independent of the current batch's file count. save_upload still enforces the per-minute sliding-window rate limit per file, so throttling is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Also reconcile the per-minute upload rate limit with the batch cap Follow-up within #1346: even after the concurrency-guard fix, a 6+ file batch still failed because save_upload() counts each file against upload_rate_limit (was 5/min) while the composer allows MAX_FILES=10 per batch — the reporter saw "5 attachments work, 6 fail". Raise the per-minute file cap to 60 so a single full batch (and a few of them) isn't self-rejected; burst abuse stays bounded by max_concurrent_uploads. Add a real 6-file regression + a config guard that the cap exceeds the frontend MAX_FILES. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,18 @@ def is_valid_upload_id(upload_id: str) -> bool:
|
||||
return UPLOAD_ID_RE.fullmatch(upload_id or "") is not None
|
||||
|
||||
|
||||
def count_recent_uploads(timestamps, now: float, window: float = 10.0) -> int:
|
||||
"""Number of upload events in *timestamps* within the last *window* seconds.
|
||||
|
||||
Used by the per-IP concurrency guard. The count is of genuine prior upload
|
||||
events — it must NOT scale with how many files are in the *current* request,
|
||||
or a single multi-file batch would reject itself (issue #1346)."""
|
||||
if not timestamps:
|
||||
return 0
|
||||
cutoff = now - window
|
||||
return sum(1 for t in timestamps if t > cutoff)
|
||||
|
||||
|
||||
class UploadHandler:
|
||||
def __init__(self, base_dir: str, upload_dir: str):
|
||||
self.base_dir = base_dir
|
||||
@@ -50,7 +62,13 @@ class UploadHandler:
|
||||
self.max_upload_size = 10 * 1024 * 1024 # 10MB
|
||||
self.max_concurrent_uploads = 3
|
||||
self.cleanup_days = 30
|
||||
self.upload_rate_limit = 5 # Max 5 uploads per minute per IP
|
||||
# Per-IP per-minute cap. save_upload() counts EACH file, and the chat
|
||||
# composer lets a user attach up to MAX_FILES (10, static/js/fileHandler.js)
|
||||
# in one batch — so this must comfortably exceed 10, or a single 6+ file
|
||||
# attach is rejected mid-batch (issue #1346: "5 work, 6 fail"). Burst abuse
|
||||
# is separately bounded by max_concurrent_uploads. Headroom for a few full
|
||||
# batches per minute.
|
||||
self.upload_rate_limit = 60 # max 60 file-uploads per minute per IP
|
||||
self.upload_rate_window = 60 # 60 seconds
|
||||
|
||||
# Track upload rates
|
||||
|
||||
Reference in New Issue
Block a user