RequestRejectedError 丢掉了 400 的响应体,导致这类失败事后不可诊断 #10
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
版本
polygateway[redis,postgres,structured]==1.1.2现象
_status_to_error(polygateway/transports/openai_compat.py:131-145)已经把响应体作为
body_text参数收进来了,_translate_429那一支也确实用了它(
:111-121,靠解析 body 里的error.type区分配额耗尽和普通限速)。但 400 那一支没有用:
:144那条 4xx 兜底同样没有用:RequestRejectedError(polygateway/errors.py:66)只从基类继承了source_name/status_code/operation,没有携带响应体的地方。该模块也没有任何 logger 调用。所以响应体在这一层之后就不存在了,
下游拿到的信息只有一句「请求被拒: 400」。
为什么这件事有代价
响应体里的信息量是足够定位问题的。我们用一个故意构造的坏 data URL 撞出来的是:
而实际生产里遇到的那次 400,因为响应体没有被任何地方保存,已经永远查不到原因了。
我们的场景:一轮 1050 张医学影像的批处理,其中 1 张在读表格这一步收到 400、
被判定为确定性失败而放弃。事后想知道「这张图到底哪里不合规」,无从查起。
顺带一个可能对库有价值的观察
那次 400 不是确定性的。我们把同一份字节(sha256 核对过一致)重发了 15 次,
15 次全部成功。
旁证:那次调用
prompt_tokens和completion_tokens都是 0、耗时 2996 ms,而同一批 631 次成功调用的最快一次是 7366 ms、没有任何一次低于 5 秒。
说明请求在推理开始之前就被挡了。
我们的部署在模型供应商前面还隔着一个第三方 API 中转服务,
中转服务自己抖动的时候也会回 400,从状态码上和供应商说「你的输入有问题」分不开。
RequestRejectedError的 docstring 现在写着「不重试不换源」。对直连供应商的部署这大概是对的;对经中转的部署,这个语义会让偶发抖动变成永久失败。
我们下游已经自己改掉了分类(不再把库的 400 当成确定性失败),
所以这一条不是请求你们改行为,只是把观察同步过来——
如果别的使用方也在中转后面,这个语义可能值得在文档里加一句提醒。
建议的方向(具体怎么做由你们定)
想要的最小能力是:拿到 400 的时候,能知道对方说了什么。
想到两个方向,都不改现有语义:
一、让异常带上截断后的响应体。 库里已经有这个先例——
ResultInvalidError(
polygateway/errors.py:73)就带着raw_text。同样给RequestRejectedError加一个字段(比如
body_text,截断到几百字符),就能让下游把它记进日志和遥测。二、在这一层记一条日志。 如果不想动异常的形状,
在
_status_to_error里对非 2xx 打一条带响应体的 WARNING 也能解决我们的问题。我们更倾向第一种,因为异常会被下游写进遥测表,而日志和遥测是两套留存。
但这是你们的接口设计,按你们觉得合适的来。
我们这边现在怎么处理的
不再把库的
RequestRejectedError(400 那一支)当成确定性失败,让它落进我们自己的兜底分类,重投到失败预算耗尽为止。
这解决了「偶发抖动导致数据丢失」,但不解决「事后查不出原因」——
后者需要上面那个能力。
已在 1.2.0 修复并发布。
根因是三条留存通道同时为空:
_status_to_error拿到了body_text却只在 429 分支用于类型细分,该模块没有任何 logger 调用,异常类也没有承载响应体的字段。且库的逐次遥测写的是str(exc)——所以只给异常加字段并不会让响应体进遥测表,两者必须都做。修复内容:
PolyGatewayError基类新增body_text字段,承载非 2xx 响应体的摘要;与ResultInvalidError.raw_text分工明确(前者是对方拒绝的理由,后者是 2xx 但内容不可解析时的模型输出)。scope 级错误(GatewayUnavailableError一族)恒为空串,它们没有单一响应体可言。error列直接可查,下游不必为此单独埋点。insufficient_quota)与 OCR 的全部分支都是同一缺陷的其余分支,一并修了。code/request_id收在尾部,头部硬切会正好切掉向网关方追查时唯一有用的那部分。关于「那次 400 不是确定性的」这个观察:
body_text现在正是区分它的判据——中转服务抖动的响应体与供应商的invalid_request_error在正文上是能分开的,尽管状态码相同。这一点写进了errors.py的 docstring。请升级到 1.2.0。