Skip to main content

先说结论:错误原文只在响应体里出现一次

API易 的错误信息只通过接口响应体返回给你。后台日志是计费账本——它记录的是成功产生扣费的调用,报错请求既不扣费、也不会出现在日志里。所以「后台日志里查不到」并不等于「没发生」,而是意味着那次错误的唯一记录就在你的客户端。你没把它打印下来、落到盘上,它就永久消失了——连我们也找不回来。
一句话结论:把接口返回的原始响应体原样打出来,不要只留你程序包装过的那一句话。400 Bad Request 这种字符串对定位问题的贡献接近于零,真正的答案在它下面被丢掉的那个 JSON 里。

一个真实案例:400 Bad Request 说明不了任何问题

一位客户上报的原文只有一行:
这行字符串是客户端框架包装后的产物。它保留了模型名、HTTP 方法、URL 和状态码,唯独把最关键的响应体丢掉了。支持侧能给出的最好回答只能是:
400 一般是内容安全和参数问题,大概率是内容安全。
这是猜测,不是结论。因为同一次调用,接口真正返回的响应体可能是下面三种中的任意一种,而它们对应三种完全不同的处理动作:
同一个 400,三种完全不同的处理动作。 丢掉响应体,就是把「三选一」变成「靠猜」——猜错的代价是一轮无效的来回沟通,外加一次本来可以避免的重试。更要紧的是:这三种情况里有两种根本不该重试。分不清是哪一种,就只能盲目重试,白白消耗时间和额度。

后台日志能查到什么,查不到什么

这是最需要先建立的认知:后台日志是计费账本,不是错误日志。
反过来用,这是排查断连类问题最有力的判据:如果日志里这条计费记录,说明请求确实到达了上游并产生了消耗;如果没有,那问题多半发生在到达上游之前(网络、鉴权、参数校验)。完整口径见怎么看懂日志里的计费金额

必须留存的 7 个字段

排查一次报错需要的信息就这些。缺任何一项都会让排查退化成猜测:
响应体不要截断。 常规业务日志里截断到 200 字符是合理的,但排障场景下错误详情经常出现在末尾。至少保留前 2000 字符;图片类接口若担心 base64 刷屏,只在 status_code >= 400 时全量打印即可——错误响应体本来就不长。

正确的错误捕获写法

核心原则只有一条:分两层捕获,并且在任何一层都不要丢弃原始信息。
  • 传输层异常:连接被重置、TLS 握手失败、超时、DNS 失败。此时根本没有 HTTP 响应,能留的只有异常原文。
  • HTTP 层错误:服务端返回了 4xx / 5xx。此时一定有响应体,必须读出来。

Python / requests

不要在读响应体之前就 raise_for_status() 它抛出的 HTTPError 只带一句 400 Client Error: Bad Request for url: ...,正文原封不动地留在 resp.text 里没人去读——这正是本页开头那个案例的成因之一。要用它,也请先把 resp.text 取出来。

Python / OpenAI SDK

官方 SDK 已经把三样东西都挂在异常对象上了,只是很多人只 print 了一句自己的中文提示:
即便只写一行,也请写 print(f"API 错误:{e}") 而不是 print("调用失败")——SDK 异常的 str(e)已经包含服务端返回的 message。真正会丢信息的是把异常对象整个扔掉的那种写法。

Node.js

用 SDK 时:
直接用 fetch 时,这一步是最容易出事的地方
本页开头那个 400 Bad Request from POST https://api.apiyi.com/v1/images/edits,字面上就等于 ${resp.status} ${resp.statusText} from ${resp.method} ${resp.url} ——响应体从头到尾没有被读取过fetch 在 HTTP 层面出错时不会 rejectresp.okfalse 而已;如果这时直接抛 resp.statusText,body 就随着响应对象一起被丢弃了。在抛错之前先 await resp.text(),这一行的差别就是能不能定位问题。

cURL 复现

请客户复现时,给这条命令最省事——它把状态码、响应头、响应体、耗时一次性全带出来:
  • -i 打印响应头,x-request-id 就在里面;
  • -sS 关掉进度条但保留错误输出;
  • -w 在末尾附加状态码与总耗时,方便和超时配置对照。

封装工具与自研中台怎么办

一个正面例子

下面这条报错来自某位客户的 ComfyUI 节点:
它比 400 Bad Request 难看得多,但信息是完整的,几秒钟就能定死方向: 结论直接就出来了:这是传输层问题,与内容安全、与参数统统无关,也不会产生计费(请求根本没完成)。排查路径见图片 API 连接中断排查
对比一下:一个是包装得很干净但什么也说明不了的 400 Bad Request,一个是又长又丑但直接指向根因的 errno 10054排障场景下,原始、难看、完整的错误远胜于友好、简洁、被改写过的错误。

常见工具去哪里找原文

自研中台的三条原则

1

透传,不要改写

中间层可以追加上下文(哪个业务、哪个租户、第几次重试),但不能替换上游返回的 error.message。一旦改写,原文就没有第二个地方可以找回。
2

面向用户的提示和面向开发的原文分开存

参考 Gemini 图片错误处理 里的三分结构:userMessage(给终端用户看的友好文案)、devMessage(给开发看的判定结论)、rawResponse(原始响应体,一字不改)。前两个可以随便润色,第三个必须原样入库。
3

永远不要出现「未知错误」

走到兜底分支时,把 statusx-request-id 和响应体前 2000 字符一并记下来。一个带着原文的「未分类错误」是可排查的;一句干净的「未知错误」不是。

反面清单:这些写法会让问题无法排查

  • except Exception as e: print("调用失败") —— 异常对象整个被丢掉,连是哪一层的问题都不知道;
  • 只记 HTTP 状态码,不记响应体 —— 就是本页开头那个案例;
  • raise_for_status() 之前不读 resp.text —— 正文还在内存里,就是没人取;
  • fetchif (!resp.ok) throw new Error(resp.statusText) —— body 随响应对象一起被扔了;
  • 客户端重试成功后只留一条漂亮的 200 —— 把每次尝试单独记一条,否则你永远看不到底层断了多少次,还容易把自己的重试误读成渠道行为;
  • 日志只打屏不落盘、或按天覆盖 —— 等客户反馈到你这里时,原始记录往往已经滚没了;
  • 报障时只发一张手机拍的屏幕照片 —— 请直接复制文本,截图里的错误经常正好被裁掉半行。

什么时候找客服

先自己走完上面的留存与判读,如果满足下面任一条,就带着材料找客服核查:
  • 拿到了完整响应体,但 error.message 指向上游方向(upstream_error、上游 5xx 原文、渠道明确报错);
  • 同一份请求参数换个模型或换个时间段就正常,只有某个特定模型稳定失败;
  • 报错是 500 + write_response_body_failed 这类下行链路问题,且稳定复现(这类不计费,排查方法见连接中断排查);
  • 你怀疑计费与实际调用对不上——这时 request_id 是唯一能精确对账的锚点。

报障信息模板(可直接复制)

企业微信客服

企业微信客服二维码扫码添加,或点击本卡片联系企业微信客服。也可通过 Telegram @apiyi001 或邮箱 [email protected] 联系我们。
把上面模板里的内容以文本形式发过来,比任何描述都高效。有 request_id 时我们能直接定位到那一次调用的完整链路,不必再问「大概几点调用的什么模型」。request_id 的查询方法见日志查询 API

相关文档

API 手册

常见错误码对照表、认证方式与速率限制

连接中断排查

ECONNRESETerrno 10054、SSL EOF 这类传输层报错的完整排查路径

日志查询 API

用接口批量拉调用日志,request_id 怎么查、怎么对账

看懂日志计费金额

为什么失败调用不进日志,以及「有无计费记录」这条判据怎么用

超时怎么配置

各类模型的 timeout 分档、已调大仍超时的逐层排查

图片 API 必读

同步调用、base64 前缀差异、400 invalid_image_file 的图片预处理