{"record":{"id":"7ab9b9a5725e7f2a","repo":"odysseus-dev/odysseus","slug":"no-files-uploaded","errorCode":null,"errorMessage":"No files uploaded","messagePattern":"No files uploaded","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/upload_routes.py","lineNumber":267,"sourceCode":"            return image_id\n        except Exception as e:\n            db.rollback()\n            logger.warning(\"Failed to add chat image upload to gallery: %s\", e)\n            return None\n        finally:\n            db.close()\n    \n    @router.post(\"\")\n    async def api_upload(\n        request: Request,\n        files: List[UploadFile] = File(...),\n        session_id: Optional[str] = Form(None),\n    ):\n        \"\"\"Upload files with enhanced security and organization.\"\"\"\n        if not isinstance(session_id, str):\n            session_id = None\n        if not files:\n            raise HTTPException(400, \"No files uploaded\")\n            \n        client_ip = request.client.host if request.client else \"unknown\"\n        out = []\n\n        # Limit concurrent uploads per IP. Count genuine recent upload events —\n        # NOT the number of files in this batch. The previous check summed over\n        # `files`, so a single multi-file request counted itself as N concurrent\n        # uploads and tripped the limit (issue #1346: \"attach more than one file\n        # → the model doesn't even see them\"). save_upload still enforces the\n        # per-minute sliding-window rate limit per file.\n        recent_uploads = count_recent_uploads(\n            upload_handler.upload_rate_log.get(client_ip, []), time.time()\n        )\n\n        if recent_uploads >= upload_handler.max_concurrent_uploads:\n            raise HTTPException(\n                status_code=429,\n                detail=f\"Maximum concurrent uploads ({upload_handler.max_concurrent_uploads}) exceeded\"","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/upload_routes.py#L249-L285","documentation":"HTTP 400 from POST /api/uploads when the multipart form contains no file parts — FastAPI's File(...) dependency yields an empty list and the handler rejects it before any rate limiting or storage work.","triggerScenarios":"Sending the multipart body with zero file fields; using a form field name other than 'files' (the endpoint binds the literal name 'files'); sending JSON instead of multipart/form-data so FastAPI binds no files.","commonSituations":"Frontend FormData appending files under a different key or forgetting to append before fetch; empty file-input submission without client-side validation; curl command missing -F 'files=@...' parts.","solutions":["Append each file as a FormData entry named exactly 'files'","Client-side guard: block submit when no file is selected","Send Content-Type: multipart/form-data — let the browser/curl set it; do not force application/json"],"exampleFix":"# before\ncurl -X POST https://host/api/uploads -F session_id=abc          # 400 No files uploaded\n# after\ncurl -X POST https://host/api/uploads -F session_id=abc -F 'files=@doc.pdf'","handlingStrategy":"validation","validationCode":"const files = fileInput.files;\nif (!files || files.length === 0) { showUserWarning('Select at least one file'); return; }\nconst fd = new FormData();\n[...files].forEach(f => fd.append('files', f));","typeGuard":"function hasFiles(files: FileList | null): files is FileList & { length: number } {\n  return !!files && files.length > 0;\n}","tryCatchPattern":"if (resp.status === 400 && body.detail === 'No files uploaded') { fixFormDataFieldNames(); }","preventionTips":["The multipart field name must be exactly 'files'","Always client-side validate non-empty file inputs before submit","Send multipart/form-data, not JSON"],"tags":["http","uploads","validation","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}