OpsKitPro logo
OpsKitPro_
返回博客
Engineering2026-07-108 分钟

诊断工具的公开 API 错误契约:让脚本、CI 和 Agent 都能读懂失败

诊断 API 如果要服务脚本、CI 和 AI Agent,就需要用稳定、可机器读取的错误结构区分参数错误、阻断目标、超时、限流和上游失败。

这篇文章按设计、实现和用法完整展开,可以直接往下读。
打开工具
诊断工具的公开 API 错误契约:让脚本、CI 和 Agent 都能读懂失败
关于正文

这篇文章按完整正文来整理,从背景、实现到用法都可以连起来读。

正文

诊断 API 会以可预期的方式失败

公开诊断 API 天然是给自动化使用的。

人可以打开网页,阅读结果,然后判断下一步怎么做。但脚本、CI 任务、告警机器人或 AI Agent 需要更稳定的信号。它们需要知道一次请求失败,是因为输入非法、目标被安全策略阻断、探测超时、调用方被限流,还是上游目标不可达。

这些不是同一种失败。

如果它们全部返回一个泛泛的 500,再附上一段自然语言说明,API 就很难被安全地使用。客户端会开始解析文案、错误重试、隐藏真实失败,或者把安全阻断误判成临时网络问题。

所以公开诊断 API 需要错误契约。

错误结构是产品契约的一部分,不是实现细节。

简单稳定的 envelope 对所有调用方都有帮助

最有用的 API 结构,往往是朴素且一致的。

成功响应里,客户端可以预期拿到结果对象。可预期的失败里,客户端也应该拿到稳定的错误对象。

例如:

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please retry later.",
    "retryAfter": 42
  }
}

具体字段可以演进,但原则不应该变:

  • success 告诉调用方这次请求是否产生了可用结果
  • error.code 给自动化一个稳定分支
  • error.message 给人类一个安全解释
  • retryAfterfieldevidence 这类可选字段提供上下文,而不是逼客户端解析自然语言

这对诊断工具尤其重要,因为很多失败都是业务领域内的正常情况。错误域名、被阻断目标、超时、跳转和上游错误,都会经常出现。

它们不应该看起来像服务器崩溃。

参数错误应该是可修正的

参数错误通常是用户可以修正的。

如果调用方提交了空域名、非法 URL、不支持的记录类型或格式错误的 IP 地址,API 应该明确说明。

一个有用的参数错误,应该回答:

  • 哪个输入不正确
  • 违反了什么规则
  • 调用方修改输入后是否可以重试

例如:

{
  "success": false,
  "error": {
    "code": "INVALID_TARGET",
    "message": "Enter a valid domain or HTTP URL.",
    "field": "target"
  }
}

这样 Web UI 可以高亮具体字段。脚本可以快速失败,而不是反复重试。AI Agent 也能知道应该修正哪个输入。

安全阻断不是网络错误

SSRF 防护可能会在真正探测前拒绝目标。

这不应该被报告成“连接失败”。

如果目标使用了不支持的协议、解析到了非公网地址、URL 里包含账号密码,或者跳转到了被阻断的位置,API 应该说明请求是被策略拒绝的。

例如:

{
  "success": false,
  "error": {
    "code": "TARGET_NOT_ALLOWED",
    "message": "The target resolves to a non-public network address."
  }
}

这个区别很重要。

被阻断目标不应该被激进重试。它也不应该被当作“网站挂了”的证据。同时,响应也不应该暴露内部 resolver 细节、私有 IP、服务端网络路径或基础设施上下文。

公开错误信息应该足够解释规则,但不应该足够用来摸清服务器。

超时需要单独分类

诊断 API 里,超时很常见。

目标可能很慢,TLS 握手可能卡住,源站可能接受连接但不返回响应,重定向链也可能消耗完探测预算。

这和参数错误不同,也和策略阻断不同。

超时响应应该告诉调用方:探测没有在允许时间内完成。

{
  "success": false,
  "error": {
    "code": "PROBE_TIMEOUT",
    "message": "The target did not respond before the diagnostic timeout.",
    "timeoutMs": 10000
  }
}

这样 CI 任务可以决定是标记为 inconclusive、重试一次,还是直接阻断发布。人也能理解:工具是用完了探测预算,而不是域名格式错误。

在诊断系统里,“因为超时所以未知”是一个真实状态。

它不应该被压缩成“失败”。

限流响应应该教客户端如何退避

限流既是保护,也是开发者体验。

当公开 API 因为调用方超过配额而拒绝请求时,响应应该包含稳定错误码和重试信息。

Header 很有用:

  • Retry-After
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset

JSON body 也应该可机器读取:

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please retry later.",
    "retryAfter": 42
  }
}

这对脚本和 Agent 很重要。看到 RATE_LIMITED,调用方可以等待。看到 INVALID_TARGET,应该修正输入。看到 TARGET_NOT_ALLOWED,应该停止。

不同错误需要不同处理方式。

上游失败应该保留证据

有些诊断请求本身成功运行了,但发现目标网站不健康。

例如:

  • DNS 解析失败
  • TCP 连接被拒绝
  • TLS hostname 匹配失败
  • HTTP 返回 502
  • 目标出现重定向循环

这些不一定是 API 失败。它们可能是一次成功诊断得到的负面发现。

这就需要另一种表达:

{
  "success": true,
  "status": "completed_with_findings",
  "result": {
    "target": "example.com",
    "findings": [
      {
        "severity": "warning",
        "code": "HTTP_5XX_OBSERVED",
        "message": "The target returned a server error.",
        "evidence": {
          "statusCode": 502
        }
      }
    ]
  }
}

API 请求成功了。网站本身可能不健康。

这个区别能让自动化更诚实。监控脚本可以说“诊断完成,并发现目标返回 502”,而不是误报成“诊断 API 失败”。

错误响应不能泄露内部信息

好的错误契约,不是把原始异常直接序列化出去。

公开响应应该避免包含:

  • stack trace
  • 原始 resolver 路径
  • 私有网络地址
  • 服务器 hostname
  • secret 名称
  • 依赖库原始异常文案
  • 内部账号或部署标识

公开 API 可以在私有日志中保留更详细的信息,但客户端响应应该只使用稳定、经过审查的字段。

对公开诊断工具来说,目标是清晰,而不是把 API 变成侦察入口。

契约应该在 route 边界测试

只测试 helper function 不够。

真正的契约,是 route 返回给外部调用方的内容:状态码、headers、JSON envelope、稳定错误码,以及非法或被阻断请求是否真的跳过了昂贵探测。

Route-level 测试可以抓住实际回归:

  • 参数错误返回一致的客户端错误
  • 被 SSRF 阻断的目标不会启动探测
  • 限流响应包含 Retry-After
  • 超时不会变成泛泛的 500
  • 可预期的上游失败会在合适场景下表现为诊断 finding

对公开 API 来说,wire response 本身就是功能。

OPC 模式下的经验

对一人产品来说,稳定错误契约是一种杠杆。

它能减少支持成本,让脚本更容易写,让 AI Agent 有更安全的分支,也能为之后的 API Key 或付费配额打基础,而不需要一开始就做很重的平台系统。

契约不需要很大。

它需要可预测:

  • 尽早校验输入
  • 明确拒绝不安全目标
  • 区分超时和策略阻断
  • 告诉被限流客户端什么时候重试
  • 探测完成时保留诊断证据
  • 不把内部信息暴露给公开响应

这样,一个小型公开 API 才能变得足够可靠,真正服务自动化。