修复 AI Gateway 图片 MIME 类型错误:用魔数检测替代扩展名猜测

修复 AI Gateway 图片 MIME 类型错误:用魔数检测替代扩展名猜测

问题背景

在使用 Hermes Discord Gateway 发送图片时,遇到了一个隐蔽的 bug:

复制代码
HTTP 400: messages.16.content.1.image.source.base64: 
The image was specified using the image/webp media type, 
but the image appears to be a image/png image

Claude API 校验失败,原因是传入的 MIME type(image/webp)与图片实际内容(PNG 格式)不符。

根本原因分析

排查路径如下:

  1. Discord CDN 的行为 :Discord 下载入站图片附件时,会根据 HTTP Response 的 Content-Type header 决定本地缓存文件的扩展名
  2. CDN 转码问题 :Discord CDN 对 PNG 图片有时返回 content-type: image/webp(Discord 内部会自动做格式转换),导致文件被存成 .webp 后缀
  3. MIME 猜测逻辑缺陷 :Gateway 的 _guess_mime() 函数纯粹基于文件扩展名判断类型,文件名叫 .webp → 返回 image/webp
  4. Claude API 严格校验:Claude 在接收 base64 图片时会校验实际字节头与声明的 MIME type 是否一致,不匹配直接 400

整个链路:文件内容是 PNG,但扩展名是 .webp → MIME 声明错误 → Claude 拒绝

修复方案:魔数(Magic Number)检测

核心思路:不再依赖文件扩展名猜 MIME type,改为读取文件头字节,通过魔数来判断真实格式。

常见图片格式的魔数

格式 字节头(Hex) 说明
PNG 89 50 4E 47 0D 0A 1A 0A 固定 8 字节签名
JPEG FF D8 FF JFIF/EXIF 前缀
WebP 52 49 46 46 xx xx xx xx 57 45 42 50 RIFF...WEBP
GIF 47 49 46 38 GIF8

修复代码

python 复制代码
def _guess_mime(path: str, data: bytes | None = None) -> str:
    """
    优先用文件头魔数判断真实 MIME type,
    兜底才使用文件扩展名。
    """
    # 读取文件头字节
    header: bytes = b""
    if data is not None:
        header = data[:12]
    else:
        try:
            with open(path, "rb") as f:
                header = f.read(12)
        except OSError:
            pass

    # 魔数匹配
    if header[:8] == b"\x89PNG\r\n\x1a\n":
        return "image/png"
    if header[:3] == b"\xff\xd8\xff":
        return "image/jpeg"
    if header[:6] in (b"GIF87a", b"GIF89a"):
        return "image/gif"
    if header[:4] == b"RIFF" and header[8:12] == b"WEBP":
        return "image/webp"

    # 兜底:扩展名猜测
    ext = os.path.splitext(path)[1].lower()
    return {
        ".png": "image/png",
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".gif": "image/gif",
        ".webp": "image/webp",
    }.get(ext, "application/octet-stream")

关键细节

魔数优先于扩展名:无论文件被命名成什么扩展名,字节头永远反映真实格式。

传递 bytes 避免重复 IO_file_to_data_url 已经读了文件内容,直接把 bytes 传给 _guess_mime,不用再 open 一次:

python 复制代码
def _file_to_data_url(path: str) -> str:
    with open(path, "rb") as f:
        data = f.read()
    mime = _guess_mime(path, data=data)   # 传入已读字节
    b64 = base64.b64encode(data).decode()
    return f"data:{mime};base64,{b64}"

实际效果

修复后,即使 Discord CDN 把 PNG 文件存成了 .webp 扩展名,魔数检测也能正确识别出 image/png,Claude API 接收正常,HTTP 400 错误消失。

经验总结

不要用文件扩展名判断文件类型------这是安全和兼容性领域的经典教训,在 AI Gateway 场景下同样成立:

  • 扩展名是用户/系统可以随意命名的元数据,不可信
  • 文件头字节是由生成工具写入的实际数据,可靠
  • CDN、代理、缓存系统在转码/转发过程中可能改变扩展名但不改变内容

这个 bug 的出现恰好是"三方联动"------Discord CDN 的转码行为 + Gateway 的扩展名猜测逻辑 + Claude API 的严格校验,三者叠加才暴露出来。单独看每一方都"没错",但组合在一起就出问题了。这类分布式系统中的隐蔽 bug,往往需要把整个数据流串起来才能找到根因。

参考

相关推荐
菩提树下的凡夫1 小时前
新一代人工智能---AI智能体
人工智能
m0_596749092 小时前
Golang怎么实现方法集与接口的匹配_Golang如何理解值类型和指针类型实现接口的区别【详解】
jvm·数据库·python
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【49】状态图运行时引擎:CompiledGraph 源码解析
java·人工智能·spring
隔壁小红馆2 小时前
隐藏odoo特有
python·odoo17·odoo18
i嘻嘻8322 小时前
餐厅送餐机器人行业研究报告
人工智能
lifewange2 小时前
pytest 找不到文件?直接在 pytest.ini 配置根目录 + 路径(最简单方案)
开发语言·python·pytest
城事漫游Molly2 小时前
研究设计核心 Toolkit:从“知道方法”到“真正会设计”
大数据·人工智能·算法·ai写作·论文笔记
love在水一方2 小时前
【导读】基于层次化多模态场景图的快慢推理视觉语言导航
人工智能
一只幸运猫.2 小时前
核心概念层——深入理解 Agent 是什么
大数据·数据库·人工智能