{"record":{"id":"1aed004bf43e1e91","repo":"harry0703/MoneyPrinterTurbo","slug":"request-id-only-files-with-extensions-joi","errorCode":null,"errorMessage":"{request_id}: Only files with extensions {', '.join(allowed_suffixes)} can be uploaded","messagePattern":"(.+?): Only files with extensions (.+?) can be uploaded","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"app/controllers/v1/video.py","lineNumber":425,"sourceCode":"    request_id = base.get_task_id(request)\n    safe_filename = _sanitize_upload_filename(file.filename, request_id)\n    # check file ext\n    allowed_suffixes = (\"mp4\", \"mov\", \"avi\", \"flv\", \"mkv\", \"jpg\", \"jpeg\", \"png\")\n    suffix = pathlib.Path(safe_filename).suffix.lower().lstrip(\".\")\n    # 按完整扩展名校验，既兼容 .MOV 这类大写后缀，也避免 photojpg 这种没有\n    # 点号的文件名因为 endswith(\"jpg\") 被误当成合法图片。\n    if suffix in allowed_suffixes:\n        local_videos_dir = utils.storage_dir(\"local_videos\", create=True)\n        save_path = os.path.join(local_videos_dir, safe_filename)\n        # save file\n        with open(save_path, \"wb+\") as buffer:\n            # If the file already exists, it will be overwritten\n            file.file.seek(0)\n            buffer.write(file.file.read())\n        response = {\"file\": safe_filename}\n        return utils.get_response(200, response)\n\n    raise HttpException(\n        \"\", status_code=400, message=f\"{request_id}: Only files with extensions {', '.join(allowed_suffixes)} can be uploaded\"\n    )\n\n@router.get(\"/stream/{file_path:path}\")\nasync def stream_video(request: Request, file_path: str):\n    request_id = base.get_task_id(request)\n    tasks_dir = utils.task_dir()\n    video_path = _resolve_path_within_directory(tasks_dir, file_path, request_id)\n    range_header = request.headers.get(\"Range\")\n    video_size = os.path.getsize(video_path)\n    start, end = _parse_byte_range(range_header, video_size, request_id)\n    length = end - start + 1\n\n    def file_iterator(file_path, offset=0, bytes_to_read=None):\n        with open(file_path, \"rb\") as f:\n            f.seek(offset, os.SEEK_SET)\n            remaining = bytes_to_read or video_size\n            while remaining > 0:","sourceCodeStart":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/controllers/v1/video.py#L407-L443","documentation":"Raised by the video-material upload endpoint when the sanitized filename's suffix is not in allowed_suffixes. The preceding comment notes a fixed subtlety: suffix is checked as a proper extension (e.g. via Path suffix), so a bare-dots filename can't sneak past as it could with naive endswith('jpg'). Existing files with a matching name are overwritten by design ('If the file already exists, it will be overwritten').","triggerScenarios":"POST /api/v1/video_materials with a .mov/.avi/.wmv (or any non-allowed) extension; a filename with no extension at all; a file named 'clip.mp4.exe' style double extensions where the final suffix fails the check.","commonSituations":"Users exporting clips from phone editors in HEVC (.mov) when only web-friendly formats are allowed; clients not validating extension before upload; uppercase-extension files if allowed_suffixes are lowercase-only.","solutions":["Convert the clip to an allowed format before upload (e.g. ffmpeg -i in.mov -c:v libx264 -c:a aac out.mp4).","Client-side pre-check: Path(filename).suffix.lower() in allowed_suffixes read from the same list the server uses.","Make sure the filename has exactly one intended extension and no trailing dots/spaces."],"exampleFix":"# before\nfiles = {\"file\": (\"holiday.mov\", open(\"holiday.mov\", \"rb\"))}\nrequests.post(url, files=files, headers=h)\n\n# after\nsubprocess.run([\"ffmpeg\", \"-y\", \"-i\", \"holiday.mov\", \"-c:v\", \"libx264\", \"-c:a\", \"aac\", \"holiday.mp4\"], check=True)\nfiles = {\"file\": (\"holiday.mp4\", open(\"holiday.mp4\", \"rb\"))}\nrequests.post(url, files=files, headers=h)","handlingStrategy":"validation","validationCode":"from pathlib import Path\nallowed = {\".mp4\", \".webm\", \".jpg\", \".jpeg\", \".png\"}  # keep in sync with server\nsuffix = Path(filename).suffix.lower()\nif suffix not in allowed:\n    raise ValueError(f\"transcode first; allowed: {sorted(allowed)}\")","typeGuard":"def is_allowed_material(filename: str, allowed_suffixes: set[str]) -> bool:\n    return Path((filename or \"\").strip()).suffix.lower() in allowed_suffixes","tryCatchPattern":null,"preventionTips":["Restrict the client file picker to allowed extensions.","Transcode phone/editor exports (HEVC/MOV) to MP4 before upload.","Watch out for double extensions — only the final suffix counts."],"tags":["upload","file-extension","http-400","video-materials"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}