文档中心内部

错误处理与避坑

最难缠的不是报错,是不报错。先讲五个静默坑,再讲哪些错该重试。

这个接口最难缠的地方在哪?

它不报错:状态码正常、图也出来、钱照常扣,只是结果不对。

📤你发请求某个参数写错了
→
✅上游返回 200响应体结构完整
→
🖼图却是错的尺寸不对
参考图被丢了
→
🔕告警一个都抓不到日志干净、钱照常扣

只有人眼看图才发现得了 —— 只能写代码时提前避开。

五个"不报错但结果是错的"坑

坑一 · size 只能用像素串,而且永远要传

✕ 这么写
"size": "16:9"

不报错,静默给你一张 2048x2048 方图。省略 size 同理:1536×864 的源图会塌成 1024×1024。

✓ 要这么写
"size": "1536x864"

只有像素串 "1536x864" 会被精确遵循。图生图也要传。

接口没有 match_input_image 这类"跟随原图"的值,比例得自己算:读源图真实像素 → 规整成合法值 → 显式传,算法见接口参考。

坑二 · 多参考图可能只有第一张生效

✕ 直接当多图已生效

中转层没透传 image[] 时,第二张之后被悄悄丢掉。照常 200、照常计费,没有任何信号。

✓ 先人工验一次

拿纯红方块 + 纯蓝圆形,提示词要求两个都清晰可见,看结果里是不是都在。

退路:改用单个 image 发底图,其余信息写进提示词;或本地拼成一张再发。image[] 的第一张(接口参考写作 images[0])就是底图 —— 顺序有语义,最重要的排最前。

坑三 · n 不存在,多张图就是多次请求

✕ 这么写
"n": 4

要 4 张,只回来 1 张。n 这个参数根本不存在。

✓ 发 4 次请求

每次单独计费。并发 3–5 个,开多了撞限流。部分失败如实暴露给调用方,别吞掉当成功。

12次
4 张 × 最多试 3 次 = 最坏 12 次账单。abort 之后上游那次可能已跑完并计费,重试等于第二次付钱。重试上限别设大,超时宁可设长(300 秒)。

坑四 · moderation 基本不可控

✕ 传了就当审核放宽了

中转层可能接受 moderation 却忽略它。你以为放宽了继续发边界内容,每次被拦、每次计费。

✓ A/B 对比确认

同一句被拦过的提示词,一次带 moderation: 'low'、一次不带。结果一样就是被忽略了 —— 生产代码别传它。

坑五 · 返回的图片 url 是临时的

✕ 存进数据库当长期地址

url 存活以小时计,域名也不固定。今天入库,明天用户点开是破图。

✓ 拿到当下就下载转存

转存到自己的存储再入库,也别直接发给终端用户。概览页示例代码就是这么写的。

错误处理矩阵

分错类别的代价不对称:故障判成永久失败 = 用户手动重跑;审核当成故障重试 = 每试一次付一次钱,一次都不会成功。

上游返回能重试吗怎么办
451不能 内容被安全审核拦截。重跑无用,改提示词或换参考图
400 + 审核关键词不能 同上,只是换了个状态码穿出来
400 + 超时 / 内部错关键词能,要有上限 这是故障伪装成 400,见下面的顺序规则一
400 参数错(尺寸 / 模型名 / 提示词太短)不能 修请求本身。原样重发必然再错一次
400 源图问题(格式不支持 / 无法解码)不能 换一张参考图,或按真实字节重新定 MIME 和后缀
401 / 403不能 key 错了或没权限,找管理员
402不能 余额不足,找管理员
429能 指数退避 + 随机抖动,别用定长间隔硬撞
5xx能,要有上限 上游临时故障
超时 / 连接被断能,要有上限 同上。前提是你的 fetch 真的设了 AbortSignal.timeout

两条顺序规则

规则一 · 先匹"故障黑名单",再匹审核关键词

📥收到 400先别急着判死
→
1故障黑名单超时 / 内部错
可重试
→
2源图问题不重试
→
3审核关键词不重试
→
4参数错不重试

顺序反了,超时就会被永久判死。

上游超时会伪装成 400 穿出来,正文形如 Timeout when executing ... with timeout 54s。

规则二 · 短码关键词必须用词边界正则

✕ 这么写
includes('e005')

真实踩过:参数错的正文 不合法的size(traceid: 7b3e005af91c4d) 里,traceid 恰好含 e005,被误判成内容审核、提示用户"请调整提示词"。

✓ 要这么写
/\be005\b/

带词边界,只匹独立出现的短码,不会被 traceid 里的片段勾中。

可直接用的分类函数

把响应变成带 kind 和 retryable 的 Error,上层只看这两个字段。两条顺序规则靠 if 的次序生效,挪分支前先回上一节。

errors.js · 错误分类
// 审核关键词(小写匹配)。同一个逻辑错误会有多种文案变体,中英都要覆盖
const MODERATION_KEYWORDS = [
  'violate our guardrails', 'may violate our content polic', 'violated our content polic',
  'content policy', 'content_policy_violation', 'safety polic', 'blocked by safety',
  'appear to be unsafe', 'appears to be unsafe', 'flagged as sensitive', 'nsfw',
  'moderation_blocked', 'result of our safety system', "i can't help with", 'i cannot help with',
  '安全政策', '内容政策', '防护限制', '色情', '裸露', '不能帮助', '无法用于生成', '违反了我们的',
];
const MODERATION_REGEXES = [/\be005\b/];   // ← 词边界,别改成 includes(见规则二)

// ★ 必须先跑:真故障穿着 400 的皮
const TRANSIENT_400 = [/timeout|timed out|超时/, /unknown internal error|internal server error/];

const BAD_REQUEST  = [/不合法的size/, /size must be/, /invalid size/, /model .{0,64} not found/, /提示词太模糊/];
const SOURCE_IMAGE = [/invalid image file/, /unsupported image format/, /image format .* not supported/, /无法解码/];

export async function buildUpstreamError(resp) {
  let raw = '';
  try { raw = await resp.text(); } catch { /* body 读不出就只按状态码判 */ }
  // ★ 分类要跑完整 body,截断只用于打日志
  const body = raw.toLowerCase();
  const s = resp.status;

  const mk = (kind, retryable) =>
    Object.assign(new Error(`[${kind}] ${s}: ${raw.slice(0, 300)}`), { kind, status: s, retryable });

  if (s === 401 || s === 403) return mk('AUTH', false);
  if (s === 402)              return mk('INSUFFICIENT_BALANCE', false);
  if (s === 429)              return mk('RATE_LIMITED', true);
  if (s >= 500)               return mk('UPSTREAM_5XX', true);
  if (s === 451)              return mk('CONTENT_MODERATED', false);   // 状态码级判定,不看正文

  if (s === 400) {
    if (TRANSIENT_400.some((re) => re.test(body)))  return mk('UPSTREAM_TRANSIENT', true);   // ← 必须第一个
    if (SOURCE_IMAGE.some((re) => re.test(body)))   return mk('BAD_SOURCE_IMAGE', false);
    if (MODERATION_KEYWORDS.some((k) => body.includes(k))
        || MODERATION_REGEXES.some((re) => re.test(body)))
                                                    return mk('CONTENT_MODERATED', false);
    if (BAD_REQUEST.some((re) => re.test(body)))    return mk('INVALID_REQUEST', false);
    return mk('UNKNOWN_4XX', true);   // 认不出的 4xx 允许重试,理由见下文
  }
  return mk('UNKNOWN', false);
}
分类要跑完整 body,截断只用于打日志

别先 slice(0, 256) 再匹关键词。上游随时可能在正文前加一段前言,关键词被切到截断点外,整类审核识别静默失效。

最后那行 UNKNOWN_4XX 可重试,不与矩阵冲突:认得出的参数错走前面分支,落到这里的是还认不出的新错因。宁可多试一次,也别把一整类没见过的错误永久判死。

重试封装(指数退避 + 抖动)

有了 retryable,重试逻辑很薄。两点:限流的退避基数要明显大于普通故障;退避必须加抖动,否则并发请求会踩同一节拍一起重试、一起再被限流。

errors.js · 重试封装
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function withRetry(fn, { max = 3 } = {}) {
  let last;
  for (let attempt = 1; attempt <= max; attempt++) {
    try { return await fn(); }
    catch (err) {
      last = err;
      const retryable = err?.retryable === true
        || err?.name === 'TimeoutError' || err?.name === 'AbortError';   // fetch 超时
      if (!retryable || attempt === max) throw err;
      const base = err?.kind === 'RATE_LIMITED' ? 15_000 : 3_000;
      await sleep(base * 2 ** (attempt - 1) + Math.random() * 2_000);    // 指数退避 + 抖动
    }
  }
  throw last;
}

max = 3 是刻意保守:调大上限不提高成功率,只放大账单。

给终端用户看的文案

错误类别该说什么不要说什么
CONTENT_MODERATED 内容被安全审核拦截,请调整描述或更换参考图 "重试一下"(重试无用)、"系统故障"(这不是故障)
INVALID_REQUEST 请求参数不对,把具体哪一项说出来 笼统的"生成失败",用户无从下手
RATE_LIMITED / UPSTREAM_5XX 服务繁忙,正在自动重试,请稍候 让用户自己手动重试 —— 程序已经在重试了
INSUFFICIENT_BALANCE / AUTH 提示联系管理员 暴露 key、余额数字这类内部信息
审核有一定随机性,文案可以如实这么写

同一句提示词有时能过、有时被拦,手动改一改重来偶尔能过。文案可照实说"调整描述后再试一次",但别承诺"重跑一定没用";也别自动重试 —— 纯浪费。

排查速查表

现象原因处理
出图永远是方图 传了 "16:9" 这类比例别名,或者压根没传 size 改用像素串,而且恒传
图生图输出比例不对 没传 size,或者没处理 EXIF 旋转 见接口参考的尺寸规整
竖拍手机照片被当成横图处理 没做 EXIF orientation 5–8 的换轴 同上
多参考图只有第一张生效 中转层没有透传 image[] 见本页坑二
请求永远不返回 Node 的 fetch 没有默认超时 每个 fetch 都必须加 AbortSignal.timeout
调了 quality: 'low' 却没变化 该档位可能没被支持、被悄悄按默认处理了 直接传 high
图片链接过一阵就 404 返回的是临时 URL 拿到当下就下载转存,见本页坑五
一张违规图导致后面正常图也失败 把审核错误当成渠道故障、疯狂重试 按分类函数走,审核类不重试
偶发 400 说超时,手动重跑就好了 故障伪装成 400 故障黑名单必须先于审核关键词匹配,见规则一
参数错却被提示"请调整提示词" 用 includes('e005') 子串误判 改成词边界正则 /\be005\b/,见规则二
415 或提示图片不合法 文件名后缀和真实格式不一致 按真实字节定 MIME 和后缀,别一律写 .png
上线前的三条自查

一、每个 fetch 都有超时,含下载生成图那次。二、非 2xx 时把响应正文带出来。三、多参考图的透传人工看图确认过一次,别赌。

土土金内部文档 · 本页最后更新 2026-09-09

内容有误或步骤跑不通,请把页面地址和截图发给内部技术支持。