摘要
代理无法处理 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.py(main 分支;旧版 codex-vision-proxy.py 同样受影响),测试 commit:<填写>
- 系统:Linux(Windows 桌面版同样命中)
- 上游:任意 OpenAI 兼容端点(
wire_api = "responses")
复现步骤
- 将 Codex 的
base_url 指向代理(如 http://127.0.0.1:19100)。
- 在桌面端粘贴图片,或让模型调用
view_image,使请求体超过压缩阈值。
- 请求失败。
代理日志(压缩体为约 1 MB 的图片数据):
[vision-proxy] handler error: UnicodeDecodeError('utf-8', b'(\xb5/\xfd\x00X\xad\xeb\x01...', 1, 2, 'invalid start byte')
开头的 28 b5 2f fd 是 zstd 帧魔数,请求头带 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: zstd 且 function_call_output.output 含 input_image 的请求必须被改写而不是被拒绝。
本地补丁验证结果
[vision-proxy] image rewrite ok format=responses images=1 cache_entries=1
zstd 压缩的纯文本与图片请求均返回 200,文本模型能根据注入的图片描述正确作答。
摘要
代理无法处理 zstd 压缩的请求体。Codex 桌面版会对大请求体(图片粘贴 /
view_image输出)启用 zstd 压缩,而代理在请求侧只做json.loads(bytes(body))且只捕获JSONDecodeError。遇到 zstd 帧时抛出的UnicodeDecodeError不属于JSONDecodeError,异常直接逃逸到通用处理器,客户端收到误导性的502 Upstream proxy request failed。小文本请求通常不压缩,所以一切正常;一旦涉及图片就必挂——表现就是"聊天正常、看图必 502"。
环境
vision_proxy.py(main分支;旧版codex-vision-proxy.py同样受影响),测试 commit:<填写>wire_api = "responses")复现步骤
base_url指向代理(如http://127.0.0.1:19100)。view_image,使请求体超过压缩阈值。代理日志(压缩体为约 1 MB 的图片数据):
开头的
28 b5 2f fd是 zstd 帧魔数,请求头带Content-Encoding: zstd。客户端响应:
{"error": {"message": "Upstream proxy request failed", "type": "proxy_error"}}根因
请求侧完全没有处理
Content-Encoding: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解压,转发解压后的字节并剥离该头(或改写后重新压缩):两个建议一并做:
(JSONDecodeError, UnicodeDecodeError, ValueError)一起捕获,返回明确的 4xx/5xx(如Unsupported Content-Encoding: <enc>),而不是通用 502。Content-Encoding: zstd且function_call_output.output含input_image的请求必须被改写而不是被拒绝。本地补丁验证结果
zstd 压缩的纯文本与图片请求均返回 200,文本模型能根据注入的图片描述正确作答。