{"record":{"id":"2db0222dd4624f2a","repo":"ZhuLinsen/daily_stock_analysis","slug":"mime-type-list-allowed-mime","errorCode":null,"errorMessage":"不支持的图片类型: {mime_type}。允许: {list(ALLOWED_MIME)}","messagePattern":"不支持的图片类型: (.+?)。允许: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/image_stock_extractor.py","lineNumber":367,"sourceCode":"    \"\"\"\n    从图片中提取股票代码及名称（使用 Vision LLM）。\n\n    优先级：Gemini -> Anthropic -> OpenAI（首个可用）。\n    支持多 Key 轮询与重试（最多 3 次，指数退避）。\n\n    Args:\n        image_bytes: 原始图片字节\n        mime_type: MIME 类型（如 image/jpeg, image/png）\n\n    Returns:\n        (items, raw_text) - items 为 [(code, name?, confidence), ...]，raw_text 为原始 LLM 响应。\n\n    Raises:\n        ValueError: 图片无效、未配置 Vision API 或提取失败时。\n    \"\"\"\n    mime_type = (mime_type or \"image/jpeg\").strip().lower().split(\";\")[0].strip()\n    if mime_type not in ALLOWED_MIME:\n        raise ValueError(f\"不支持的图片类型: {mime_type}。允许: {list(ALLOWED_MIME)}\")\n\n    if not image_bytes:\n        raise ValueError(\"图片内容为空\")\n\n    if len(image_bytes) > MAX_SIZE_BYTES:\n        raise ValueError(f\"Image too large (max {MAX_SIZE_BYTES // (1024 * 1024)}MB)\")\n\n    _verify_image_magic_bytes(image_bytes, mime_type)\n\n    image_b64 = base64.b64encode(image_bytes).decode(\"ascii\")\n    model = _resolve_vision_model()\n    keys = _get_api_keys_for_model(model, get_config())\n\n    last_error: Optional[Exception] = None\n    for attempt in range(3):\n        try:\n            key = random.choice(keys) if keys else None\n            raw = _call_litellm_vision(image_b64, mime_type, api_key=key)","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/image_stock_extractor.py#L349-L385","documentation":"Validation error from extract_stock_codes_from_image: the normalized MIME type is not in ALLOWED_MIME (image/jpeg, image/png, image/webp, image/gif). The MIME string is lowercased and stripped of parameters (';'-suffix) before the check, so only genuinely unsupported types fail.","triggerScenarios":"Passing mime_type like 'image/bmp', 'image/tiff', 'image/heic', 'image/x-icon', or a garbage string after normalization; passing None defaults to image/jpeg and passes.","commonSituations":"User uploads HEIC (iPhone photos) or BMP/TIFF/AVIF; front-end forwarding the file.type without restriction; misdetected MIME from upload middleware.","solutions":["Convert the image to JPEG/PNG/WebP/GIF before calling (e.g. via Pillow: Image.open(x).convert('RGB').save(..., 'JPEG'))","Reject unsupported types at the upload layer before backend submission","If HEIC support is required, add server-side conversion since browsers can't decode HEIC"],"exampleFix":"# before\nraw = extract_stock_codes_from_image(data, 'image/heic')  # ValueError\n# after\nfrom PIL import Image\nimport io\nbuf = io.BytesIO()\nImage.open(io.BytesIO(data)).convert('RGB').save(buf, 'JPEG')\nraw = extract_stock_codes_from_image(buf.getvalue(), 'image/jpeg')","handlingStrategy":"validation","validationCode":"from src.services.image_stock_extractor import ALLOWED_MIME\nmime = (mime_type or '').split(';')[0].strip().lower()\nif mime not in ALLOWED_MIME:\n    convert_with_pillow_then_retry()  # or reject at upload","typeGuard":"def is_supported_image_mime(m: str) -> bool:\n    return (m or '').split(';')[0].strip().lower() in {\n        'image/jpeg', 'image/png', 'image/webp', 'image/gif'}","tryCatchPattern":"try:\n    extract_stock_codes_from_image(data, mime)\nexcept ValueError as e:\n    if '不支持的图片类型' in str(e):\n        data, mime = convert_image(data)  # Pillow -> JPEG\n        extract_stock_codes_from_image(data, mime)\n    else:\n        raise","preventionTips":["Restrict the file input accept= attribute to image/jpeg,png,webp,gif","Normalize HEIC/BMP/TIFF to JPEG server-side before extraction","Run the same MIME normalization (lowercase, strip params) the backend uses"],"tags":["validation","image","mime-type"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}