修复 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,往往需要把整个数据流串起来才能找到根因。

参考

相关推荐
花酒锄作田13 分钟前
[python]argparse 包在聊天机器人中的应用
python
久违 °2 小时前
【AI-Agent】TagMatrix 数据标注工具开发
人工智能·数据分析·go·agent·数据隐私
NiceCloud喜云2 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
AI360labs_atyun3 小时前
腾讯推出电子牛马Marvis,好用吗?
人工智能·科技·ai
Dfreedom.3 小时前
Windows、虚拟机、开发板组网通信原理及调试通联步骤
人工智能·windows·部署·边缘计算·开发板·模型加速
3DVisionary3 小时前
蓝光三维扫描:医疗制造的精度焦虑怎么解
人工智能·算法·制造·蓝光三维扫描·医疗制造·三维检测·义齿检测
Are_You_Okkk_3 小时前
基于MonkeyCode解析AI研发新模式,根治开发低效痛点
大数据·人工智能·开源·ai编程
AI玫瑰助手3 小时前
Python函数:默认参数的定义与注意事项
开发语言·python·信息可视化
ylscode3 小时前
PureLogs 信息窃取恶意软件惊现高危变种:借道 MsBuild.exe 进程空心化实施无痕攻击
网络·安全·安全威胁分析
IPHWT 零软网络3 小时前
MX60E-A信创级智能语音网关技术实现与架构分析
网络·网络安全·国产自研·技术实现·智能语音网关·政企通信·信创技术