{"record":{"id":"64ffd70a6361f18f","repo":"ZhuLinsen/daily_stock_analysis","slug":"bad-request","errorCode":"bad_request","errorMessage":"未提供文件，请使用表单字段 file 上传图片","messagePattern":"未提供文件，请使用表单字段 file 上传图片","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"api/v1/endpoints/stocks.py","lineNumber":142,"sourceCode":"    responses={\n        200: {\"description\": \"提取的股票代码\"},\n        400: {\"description\": \"图片无效\", \"model\": ErrorResponse},\n        500: {\"description\": \"服务器错误\", \"model\": ErrorResponse},\n    },\n    summary=\"从图片提取股票代码\",\n    description=\"上传截图/图片，通过 Vision LLM 提取股票代码。支持 JPEG、PNG、WebP、GIF，最大 5MB。\",\n)\ndef extract_from_image(\n    file: Optional[UploadFile] = File(None, description=\"图片文件（表单字段名 file）\"),\n    include_raw: bool = Query(False, description=\"是否在结果中包含原始 LLM 响应\"),\n) -> ExtractFromImageResponse:\n    \"\"\"\n    从上传的图片中提取股票代码（使用 Vision LLM）。\n\n    表单字段请使用 file 上传图片。优先级：Gemini / Anthropic / OpenAI（首个可用）。\n    \"\"\"\n    if not file or not file.filename:\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"bad_request\", \"message\": \"未提供文件，请使用表单字段 file 上传图片\"},\n        )\n\n    content_type = (file.content_type or \"\").split(\";\")[0].strip().lower()\n    if content_type not in ALLOWED_MIME:\n        raise HTTPException(\n            status_code=400,\n            detail={\n                \"error\": \"unsupported_type\",\n                \"message\": f\"不支持的类型: {content_type}。允许: {ALLOWED_MIME_STR}\",\n            },\n        )\n\n    try:\n        # 先读取限定大小，再检查是否还有剩余（语义清晰：超出则拒绝）\n        data = file.file.read(MAX_SIZE_BYTES)\n        if file.file.read(1):","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/stocks.py#L124-L160","documentation":"Raised by POST /api/v1/stocks/extract-from-image when the multipart form contains no field named file, or the field has no filename. The endpoint declares file as Optional[UploadFile] specifically so it can return this friendly 400 (bad_request) instead of FastAPI's default 422. It is purely a request-shape error; no bytes are read.","triggerScenarios":"Calling the endpoint with an empty body; sending the image under a different form field name (e.g. image, upload); sending JSON instead of multipart/form-data; some HTTP clients that omit filename in the Content-Disposition header.","commonSituations":"Frontend field name mismatch between the FormData append key and the API contract; curl invocations using -d instead of -F; automated tests posting JSON to a multipart endpoint; proxies stripping multipart parts.","solutions":["Send multipart/form-data with the field named exactly 'file': curl -F 'file=@screenshot.png' .../extract-from-image.","In JS, use formData.append('file', fileBlob, 'screenshot.png') so a filename is present.","Double-check the endpoint: this route wants multipart, not application/json (that is /parse-import)."],"exampleFix":"// before\nconst fd = new FormData();\nfd.append('image', file);\n\n// after\nconst fd = new FormData();\nfd.append('file', file, file.name);","handlingStrategy":"validation","validationCode":"if (!(file instanceof File) || !file.name) {\n  showError('请选择要上传的图片文件');\n  return;\n}\nconst fd = new FormData();\nfd.append('file', file, file.name);","typeGuard":"function isUploadableFile(v: unknown): v is File {\n  return v instanceof File && typeof v.name === 'string' && v.name.length > 0;\n}","tryCatchPattern":null,"preventionTips":["Always name the multipart field exactly 'file'.","Disable the upload button until a file is selected.","Include a filename when appending blobs: fd.append('file', blob, 'name.png')."],"tags":["upload","multipart","fastapi","http-400"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}