Build in Public · LF-12
API 报错后怎么处理:从 401、422 到有限自愈
分清认证、权限、能力、参数和未知结果,利用当前目录修正请求,并用一个不发网络请求的决策器约束重试,避免错误修复变成越权或重复调用。
一次请求失败后,客户端最需要回答的不是“还能试几次”,而是“这次有没有执行,以及改什么才有意义”。Key 缺失、平台名错误、参数不支持和响应途中断开,可能都进入同一个 catch;如果统一重试,就会把可修正输入变成重复失败,还可能重复创建已经存在的任务。
本文以 EveryInfra 的 REST 接口为例,给出错误分层、目录核对与有限自愈的做法。示例决策器只返回下一步建议,不发请求、不更换凭据、不自动扩大数据范围。当前源码、免费公开探针和历史业务样本分开使用,不以一个 400 响应推导所有鉴权后的失败行为。
先保留这次尝试,再分析错误
在调用前生成业务操作编号,用于把同一次业务意图下的多次尝试关联起来。每次尝试另存起止时间、请求版本、目标能力、HTTP 状态和可得的服务请求 ID。业务编号不是服务端幂等键;服务没有声明支持时,不能认为带着它重发就会去重。
响应体也可能是 HTML、文本、空内容或无法解析的 JSON。先核对 Content-Type 与状态,再尝试解析 error;解析失败时保留最小诊断摘要,不把整页错误内容直接写进用户提示或交给 Agent 当指令。日志默认不保存 Key、完整带签名 URL、原始正文及其他不必要个人信息。
当前 EveryInfra 的公开错误外壳包含 error.code、error.message、error.request_id。以 code 和 HTTP 状态组织处理,message 给人阅读;不要依赖英文消息全文不变,也不要用字符串中的第一条 URL 自动跳转并携带凭据。
这里的诊断白名单可参考 OWASP Logging Cheat Sheet:用交互标识关联事件,并对令牌、会话值与敏感信息做排除。排错信息应足以定位这次尝试,而不成为另一份可被滥用的凭据或客户数据副本。
401 与 403:认证和权限分别处理
HTTP 401 表示缺少有效的认证凭据;403 表示服务器拒绝执行。协议状态提供一般含义,具体原因还要看产品错误码。EveryInfra 当前实现中,缺 Key、无效 Key、过期 Key 属于认证问题;产品线、细分能力和来源 IP 的限制属于权限问题。
先检查请求是否发到正确服务、Authorization 是否按该服务要求设置、运行进程读取的是否是预期环境。可以检查变量存在与配置标识,不需要把秘密值打印出来。浏览器已登录控制台,也不能证明服务端进程已经获得正确 API Key。
对连续认证失败,暂停受影响任务并通知负责人;不要为了恢复运行去遍历其他 Key、换账户或关闭权限限制。自动刷新仅适用于已经明确授权且产品支持的刷新流程,不能把任意长期 API Key 当成可自行续期的令牌。
能力名错误,先读候选,再核对目标
一个无需 Key 的公开错误可以这样观察。这里故意传入旧的 google_maps 标识,只读取目录,不采集地点评论:
curl --silent --show-error --include --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=google_maps'2026-09-04 20:46(北京时间)的结果是 HTTP 400,error.code 为 unknown_capability,提示包含 google_maps_reviews 和另一个相近候选。不能把未知能力一律写成 404,也不能因为候选排第一就自动把原任务换过去。
如果原任务明确是地点评论,再核对 google_maps_reviews 是否提供 reviews、需要何种 URL,以及输出是否仍满足原要求。候选算法只解决名称相似,不理解商业目标;商品应用商店与地点评论不是可互换的数据来源。
422:让输入回到当前契约
422 的一般含义是请求内容可理解,但其中指令无法处理。应用应进一步区分缺少字段、未知字段、非法枚举和体积限制。它们的修复方式不同:缺 URL 需要真实目标;未知 sort 需要核对参数;输入过长需要确认是否允许拆分,而不是静默删掉一半材料。
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=douyin' \
| jq '[.capabilities[] | {
action, required_params, optional_params, param_meanings, max_limit
}]'当前实现会对未知参数产生 unknown_param,对不支持的枚举产生 invalid_value,并附可行动的提示。但同名参数在不同 action 中仍可能不同,不能拿一张全局参数表代替具体契约。required_params、optional_params 和允许值需要一起看;列表的通用 limit 也不是分页承诺。
修正后生成新的请求版本,保留原错误与修改理由。只在明确保持原意的情况下自动采用映射;多个候选、缺少真实目标、需要扩大时间范围或改变模型时交给人决定。把缺失字段填成模型猜测值,不是安全自愈。
402 与 429:不要把额度问题写成限流
EveryInfra 当前使用 402 quota_exhausted 表示额度不足,这是产品自身约定,不是所有服务通用的 402 业务含义。额度问题需要账户负责人处理;降低并发不会自动增加余额,客户端也不应自行充值或迁移凭据。
429 是限流信号。若实际响应提供有效的 Retry-After,按其要求安排;没有该头,不代表可以立即密集重试,也不能虚构一个服务承诺的恢复时间。重试间隔、随机抖动、最大次数和总截止时间由应用策略明确设置。
限速策略还要覆盖所有共享同一额度的工作进程。单个 worker 自己退避,但其他 worker 仍继续发送,可能没有解决实际拥塞。这里是应用调度建议,不是声称 API 已替你的多个进程完成协调。
5xx、断线和已提交任务,先确认执行状态
5xx 不能简单解释为“什么都没发生”。服务可能已经受理、产生部分结果或完成账务步骤,只是客户端没有拿到完整响应。没有明确幂等保证或未执行证据时,先记录未知结果,按请求标识核对,而不是自动再发同一个 POST。
已取得 job_id 或服务明确提供的查询入口时,优先查询原任务。正在运行、等待收尾、已成功、已失败与查询本身失败要分开;查询得到 404 也可能涉及身份或作用域,不应该直接创建替代任务。先确认查询使用原请求对应的身份。
HTTP 200 之后仍需检查业务内容。MCP 又多了 JSON-RPC 和工具错误层,不能直接使用只看 REST 状态的策略。空集合、部分结果和异常响应各保留自己的状态,不统一记作成功,也不从客户端状态直接推导最终扣款。
Microsoft 的 Retry pattern 特别提醒:只对适合恢复的故障重试,同时考虑操作幂等性和多层重试叠加。SDK、队列和业务层若各自重试,实际次数可能超出业务预算;应由理解完整上下文的一层统一决策,而非每层都默认“再试一次”。
一个只做分流、不执行重试的决策器
下面是应用层的离线示例,不是 EveryInfra SDK。输入必须来自已经完成协议解析的 REST 尝试记录;replaySafe 需要依据真实接口约定或明确未执行证据设置,不能由模型或错误文字自行打开。attempts 表示已经发生的尝试次数。
function nextStep({status, hasJob = false, replaySafe = false,
attempts = 1, maxAttempts = 3}) {
if (status !== null && (!Number.isInteger(status) || status < 100 || status > 599)) {
throw new Error("invalid HTTP status");
}
if (typeof hasJob !== "boolean" || typeof replaySafe !== "boolean"
|| !Number.isSafeInteger(attempts) || attempts < 1
|| !Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
throw new Error("invalid decision inputs");
}
if (status === 401 || status === 403) return "authorization_review";
if (status === 402) return "account_review";
if (hasJob) return "inspect_existing_job";
if (status === 400 || status === 422) return "revise_request";
if (status === 429 || status === null || (status >= 500 && status <= 599)) {
return replaySafe && attempts < maxAttempts
? "retry_after_policy_check"
: "inspect_attempt";
}
if (status >= 200 && status < 300) return "validate_delivery";
return "manual_review";
}
// 合成尝试;不包含任何网络调用。
console.log(nextStep({status: 503}));
console.log(nextStep({status: 429, replaySafe: true, attempts: 3}));
console.log(nextStep({status: 202, hasJob: true}));三个输出分别要求核对本次尝试、在预算耗尽后核对尝试、查询原任务。retry_after_policy_check 也不是立即重发的命令;外层还需检查等待时间、全局限速和用户允许的操作范围。修参数后若构造新尝试,同样计入整体业务预算,不能靠重置计数无限循环。
验收“会停下来”,和验收“能恢复”同样重要
- 无效认证:不访问其他凭据,不产生业务重试。
- 能力候选有歧义:保留原目标,要求核对,不自动换产品。
- 已受理但响应中断:保留原尝试与任务,不重复创建。
- 参数修正后仍失败:记录新版本,遵守整体尝试预算。
- 非 JSON 错误、缺请求 ID、未知 code:进入保守处理,不崩溃或伪造成功。
- 返回了可用数据但业务目标未满足:保留交付状态与后续问题,不擅自宣布退款。
维护一份小的错误样本集,并在升级 SDK、修改契约或改变认证策略时回放。源码测试能验证预期顺序,实际部署还需要限定输入的现场证据;两者日期分别记录。不要用旧截图或免费目录的错误代替今天鉴权后的业务验收。
安全自愈的目标是让明确可修复的问题少打断人,同时让真正需要判断的地方及时停下。只要每一步都能说明错误在哪里、改了什么、有没有执行以及为何允许下一步,错误提示才真正成为接入帮助,而不是无限重试的触发器。