Skip to content

Codex 桌面端含图片的请求必现 502:代理未处理 zstd 压缩请求体 #29

Description

@l246804

摘要

代理无法处理 zstd 压缩的请求体。Codex 桌面版会对大请求体(图片粘贴 / view_image 输出)启用 zstd 压缩,而代理在请求侧只做 json.loads(bytes(body)) 且只捕获 JSONDecodeError。遇到 zstd 帧时抛出的 UnicodeDecodeError 不属于 JSONDecodeError,异常直接逃逸到通用处理器,客户端收到误导性的 502 Upstream proxy request failed

小文本请求通常不压缩,所以一切正常;一旦涉及图片就必挂——表现就是"聊天正常、看图必 502"。

环境

  • 客户端:Codex 桌面版(app-server 的 HTTP 栈会对大请求体启用 zstd 压缩)
  • 代理:vision_proxy.pymain 分支;旧版 codex-vision-proxy.py 同样受影响),测试 commit:<填写>
  • 系统:Linux(Windows 桌面版同样命中)
  • 上游:任意 OpenAI 兼容端点(wire_api = "responses"

复现步骤

  1. 将 Codex 的 base_url 指向代理(如 http://127.0.0.1:19100)。
  2. 在桌面端粘贴图片,或让模型调用 view_image,使请求体超过压缩阈值。
  3. 请求失败。

代理日志(压缩体为约 1 MB 的图片数据):

[vision-proxy] handler error: UnicodeDecodeError('utf-8', b'(\xb5/\xfd\x00X\xad\xeb\x01...', 1, 2, 'invalid start byte')

开头的 28 b5 2f fdzstd 帧魔数,请求头带 Content-Encoding: zstd

客户端响应:

{"error": {"message": "Upstream proxy request failed", "type": "proxy_error"}}

根因

请求侧完全没有处理 Content-Encoding

if body:
    try:
        parsed = json.loads(bytes(body))   # zstd 帧直接抛 UnicodeDecodeError
    except json.JSONDecodeError:
        pass
  • UnicodeDecodeError 不是 JSONDecodeError,不会被吞掉,一路抛到通用 except Exception 变成 502。
  • 全仓库搜索 zstd / gzip / deflate / compress / decompress,请求侧零命中;仅有的两处 Content-Encoding(约 1108、1120 行)是响应侧防改写守卫,与本问题无关。
  • _upstream_headers 不会剥离 Content-Encoding,即使解压后转发,也需要剥离该头或重新压缩。

为什么上游没发现:请求压缩是客户端行为。CLI 发明文 JSON,只有桌面版对大请求体压缩。README 声称验证过的"真实 Codex 会话"大概率用的 CLI,所以作者和社区都没踩到;桌面版 + 图片的用户要么没升级到新代码,要么没走代理。

建议修复

解析前按 Content-Encoding 解压,转发解压后的字节并剥离该头(或改写后重新压缩):

# 请求侧
encoding = (_header_value(incoming_headers, "content-encoding") or "").strip().lower()
if encoding in ("gzip", "x-gzip"):
    raw = gzip.decompress(raw)
elif encoding == "deflate":
    try:
        raw = zlib.decompress(raw)
    except zlib.error:
        raw = zlib.decompress(raw, -zlib.MAX_WBITS)
elif encoding == "zstd":
    # 用 zstandard / libzstd 流式解压(帧可能不携带内容大小)
    ...
elif encoding == "br":
    import brotli
    raw = brotli.decompress(raw)
# 然后 parsed = json.loads(raw.decode("utf-8"))
# _upstream_headers:转发解压后的字节时剥离请求侧编码
if lower in HOP_HEADERS or lower == "content-encoding":
    continue

两个建议一并做:

  • (JSONDecodeError, UnicodeDecodeError, ValueError) 一起捕获,返回明确的 4xx/5xx(如 Unsupported Content-Encoding: <enc>),而不是通用 502。
  • 补回归测试:Content-Encoding: zstdfunction_call_output.outputinput_image 的请求必须被改写而不是被拒绝。

本地补丁验证结果

[vision-proxy] image rewrite ok format=responses images=1 cache_entries=1

zstd 压缩的纯文本与图片请求均返回 200,文本模型能根据注入的图片描述正确作答。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions