{"record":{"id":"a0b59dcef4d2955c","repo":"ZhuLinsen/daily_stock_analysis","slug":"extract-failed","errorCode":"extract_failed","errorMessage":"extract_failed","messagePattern":"extract_failed","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"api/v1/endpoints/stocks.py","lineNumber":189,"sourceCode":"        logger.warning(f\"读取上传文件失败: {e}\")\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"read_failed\", \"message\": \"读取上传文件失败\"},\n        )\n\n    try:\n        items, raw_text = extract_stock_codes_from_image(data, content_type)\n        extract_items = [\n            ExtractItem(code=code, name=name, confidence=conf) for code, name, conf in items\n        ]\n        codes = [i.code for i in extract_items]\n        return ExtractFromImageResponse(\n            codes=codes,\n            items=extract_items,\n            raw_text=raw_text if include_raw else None,\n        )\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail={\"error\": \"extract_failed\", \"message\": str(e)})\n    except Exception as e:\n        logger.error(f\"图片提取失败: {e}\", exc_info=True)\n        raise HTTPException(\n            status_code=500,\n            detail={\"error\": \"internal_error\", \"message\": \"图片提取失败\"},\n        )\n\n\n@router.post(\n    \"/parse-import\",\n    response_model=ExtractFromImageResponse,\n    responses={\n        200: {\"description\": \"解析结果\"},\n        400: {\"description\": \"未提供数据或解析失败\", \"model\": ErrorResponse},\n        500: {\"description\": \"服务器错误\", \"model\": ErrorResponse},\n    },\n    summary=\"解析 CSV/Excel/剪贴板\",\n    description=\"上传 CSV/Excel 文件或粘贴文本，自动解析股票代码。文件上限 2MB，文本上限 100KB。\",","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/stocks.py#L171-L207","documentation":"Raised by /extract-from-image when extract_stock_codes_from_image raises a ValueError. That service signals expected, caller-facing failures via ValueError (per the endpoint's contract), and its message is passed through verbatim as the 400 detail with error code extract_failed — typical messages include unsupported MIME after content sniffing and 'no vision LLM configured'.","triggerScenarios":"Posting a file whose declared Content-Type is allowed but whose actual bytes are not a decodable image (corrupt or misnamed file); running the API without any vision-capable LLM credentials (Gemini/Anthropic/OpenAI) so the extractor cannot be constructed; the vision provider returning content the parser rejects.","commonSituations":"A .png that is actually a renamed PDF or text file; missing/invalid API keys in env for all three vision providers; truncated downloads; zero-byte uploads that pass the earlier checks.","solutions":["Read the passthrough message: if it names a missing provider/key, configure one vision LLM credential (Gemini/Anthropic/OpenAI) and retry.","Verify the payload is a real image: open it locally (PIL.Image.open(...).verify()) before sending.","Re-download or re-export the source image if it is corrupt."],"exampleFix":"# before\nwith open('quote.png', 'rb') as f:  # actually a renamed PDF\n    requests.post(url, files={'file': f})  # extract_failed\n\n# after\nfrom PIL import Image\nImage.open('quote.png').verify()  # raises here -> fix the file first\nwith open('quote.png', 'rb') as f:\n    requests.post(url, files={'file': f})","handlingStrategy":"validation","validationCode":"from PIL import Image\ntry:\n    img = Image.open(io.BytesIO(data)); img.verify()\nexcept Exception:\n    raise ValueError('文件不是有效图片，请重新导出')\n# plus: confirm a vision LLM key is configured before enabling the upload UI","typeGuard":"async function isDecodableImage(bytes: ArrayBuffer): Promise<boolean> {\n  try { await createImageBitmap(new Blob([bytes])); return true; }\n  catch { return false; }\n}","tryCatchPattern":"resp = requests.post(url, files={'file': fh})\nif resp.status_code == 400:\n    err = resp.json().get('detail', {})\n    if err.get('error') == 'extract_failed':\n        showUserMessage(err.get('message'))  # provider/config guidance passthrough\n        return\nraise_for_status_otherwise(resp)","preventionTips":["Verify the image decodes locally before uploading.","Check that at least one vision provider key (Gemini/Anthropic/OpenAI) is configured before exposing the feature.","Do not rely on file extensions — validate magic bytes."],"tags":["llm","vision","image","http-400"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}