Fix multi-file uploads tripping the per-IP concurrency guard (#1346) (#1362)

* 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:
lekt8
2026-06-03 03:04:19 +08:00
committed by GitHub
parent fd37ccebae
commit 0ec8415f0e
3 changed files with 196 additions and 9 deletions

View File

@@ -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