Skip to content

🚦 退出码

ipapi CLI 用一组语义化的进程退出码(exit code)告诉你"这次调用到底成了没、不成是什么性质的失败"。0 是成功,2 是用法错,3-12 是各类业务错误,70 是内部异常——脚本里 echo $? 一行就能分流处理,不用去解析输出。

退出码是 CLI 与 shell 脚本、CI/CD 流水线之间的"契约":它把"成功 / 参数错 / 输入非法 / 限流 / 上游故障 / 鉴权失败 / 内部异常"这些本质不同的情形,映射到 13 个可枚举的整数上。配合 stderr 错误信封,你既能用退出码做粗粒度分流,又能用信封里的 code / sentinel / retryable 做细粒度决策。

  • 🔢 13 个码各司其职0 成功,2 用法,3-5 输入校验(IP/字段/格式),6-9 运行时(限流/保留段/未找到/上游错误),10-12 协议层(方法/Key/数据),70 兜底内部。
  • 🟢 可重试仅 3 个6 RATE_LIMITED8 NOT_FOUND9 SERVER_ERROR 标记 retryable: true,其余皆确定性错误。
  • 🧭 与 SDK 同源:CLI 的退出码直接来自 pkg/ipapi 的哨兵错误(ErrInvalidIP 等),语义与 /api/errors 一一对应。
  • 📤 stdout 永不污染:失败时 stdout 不输出任何内容,错误信封只走 stderr——管道安全。

🚀 一行装好

bash
go install github.com/cyberspacesec/ipapi.co-skills/cmd/ipapi@latest

📋 完整退出码表

下表列出全部 13 个退出码。脚本里 echo $? 判断结果时对这张表。

code含义retryable触发条件
0成功命令正常返回,stdout 输出成功信封或原始字节
2USAGE用法错误参数缺失 / 多余参数 / 子命令拼错 / raw 未指定 -f
3INVALID_IPIP 非法ipapi info 999.1.1.1(不是合法 IPv4/IPv6)
4INVALID_FIELD字段名非法ipapi field 8.8.8.8 foo(不在 28 字段白名单内)
5INVALID_FORMAT格式非法ipapi raw 8.8.8.8 -f txt(仅支持 json/jsonp/xml/csv/yaml)
6RATE_LIMITED触发限流上游 429 或限流响应,匿名调用过频最常见
7RESERVED_IP保留 IP 段ipapi info 10.0.0.1(私有/回环/链路本地等保留段)
8NOT_FOUND未找到上游 404,该 IP 在 ipapi.co 暂无数据
9SERVER_ERROR上游错误上游 5xx 或 400,服务端临时故障
10METHOD_NOT_ALLOWED方法不允许上游 405,正常调用不会触发
11INVALID_KEYAPI Key 无效上游 403 / Invalid Key,Key 过期、拼错或额度耗尽
12UNEXPECTED_DATA数据解析失败返回体无法解码为期望的 IPInfo 结构
70INTERNAL内部异常未预期的 panic / 兜底错误,应上报 issue

⚠️ 270 的区别

2(USAGE)是"你用错了"——参数层面的问题,修你的命令行就行。70(INTERNAL)是"CLI 自己出了问题"——属于 bug,正常使用下不应见到;若反复出现,请到 GitHub Issues 上报,附上命令、版本(ipapi version)与 stderr 信封。


🟢 可重试 vs ❌ 不可重试

退出码 6 / 8 / 9 标记 retryable: true,是仅有的三个可重试码。它们的共同点是"瞬时性"——这次失败不代表下次还失败:

为什么可重试典型恢复时间
6 RATE_LIMITED限流按窗口计数,过窗口即恢复秒级 ~ 分钟级
8 NOT_FOUND上游偶发 404,重试可能命中秒级
9 SERVER_ERROR上游 5xx 通常是瞬时抖动秒级 ~ 分钟级

其余 10 个码(2/3/4/5/7/10/11/12/70,外加成功的 0)都是确定性的——同样的输入重试一千次还是同样结果。对它们,重试是浪费,应该直接修正输入或换 Key。

🔍 为什么 NOT_FOUND 也算可重试?

严格说,"这个 IP 查不到"对单个 IP 而言是确定的。但 ipapi.co 把 404 也用在"上游临时无此数据"的情形上——配合短暂退避重试,有时能拿到结果。CLI 沿用 SDK 的 IsRetryableError 语义,把 ErrNotFound 列为可重试,给瞬时性 404 一个恢复机会。如果你的业务确定某 IP 不可能短期变化(比如保留段已被 7 拦截),把 8 当不可重试处理也合理。

retryable 的退避建议

6 / 8 / 9 三个可重试码,建议采用指数退避 + 抖动策略,而不是立刻无脑重打:

bash
# 朴素的指数退避:1s → 2s → 4s,最多 3 次
backoff=1
for i in 1 2 3; do
  ipapi info 8.8.8.8 > /tmp/out.json 2>/tmp/err.json
  rc=$?
  if [ $rc -eq 0 ]; then
    cat /tmp/out.json; exit 0
  fi
  if [ $rc -eq 6 ] || [ $rc -eq 8 ] || [ $rc -eq 9 ]; then
    # 加一点抖动,避免雷同的重试风暴
    sleep $(( backoff + (RANDOM % 100) / 100 ))
    backoff=$(( backoff * 2 ))
    continue
  fi
  # 不可重试码:直接退出,别浪费
  cat /tmp/err.json >&2
  exit $rc
done
echo "重试耗尽" >&2
exit 1

🧰 优先用内置 --retries

CLI 自带 --retries(默认 2,总请求数 = retries + 1),SDK 层已实现重试逻辑,且只对 IsRetryableError 为真的错误重试。多数情况下你不需要手写上面的循环——直接:

bash
ipapi info 8.8.8.8 --retries 4 --timeout 30s

只有当你想在 shell 层做更精细的退避(比如跨进程协调、按码分流)时,才需要自己写循环。内置重试不退避抖动,若你担心密集重试触发更严限流,再叠加外层退避。

退避参数推荐值说明
初始间隔1s第一次重试前等 1 秒
退避倍率×2每次翻倍:1s → 2s → 4s
抖动±100ms避免多客户端同步重试
最大重试次数3-5超过即放弃,转降级
单次超时--timeout 30s配合 --retries 3

🧭 错误处理决策树

一条命令失败后,该"重试"还是"修输入"?顺着这张图走:

🎯 一句话决策

  • 码 ∈ {6, 8, 9} → 退避重试
  • 码 ∈ {2, 3, 4, 5, 7, 11} → 修输入 / 换 Key,别重试
  • 码 ∈ {10, 12, 70} → 正常不该出现,留 issue
  • 码 = 0 → 收工

从错误类型到退出码

上图是"退出码 → 动作"的视角。反过来问:SDK 抛了一个错误,CLI 最终会落到哪个退出码?关键是 errors.Is 逐哨兵判定——第一个匹配的哨兵即决定出口码,匹配不上则兜底 70

🧬 errors.Is 而非类型断言

SDK 的哨兵错误用 fmt.Errorf("%w", …) 包裹了 op(操作名)等上下文,CLI 用 errors.Is(err, ErrInvalidIP) 这种按哨兵比对的方式匹配,而非直接类型断言——这样上层包装过的错误也能正确归因。详见 /api/errorsWrapError 与哨兵定义。


🚦 退出码状态机

把 13 个码按"语义家族"归为四个状态:0 成功终态、2 用法分支、3-12 业务错误簇、70 兜底异常。6 / 8 / 9 是簇内仅有的"可重试"自循环态。

🎯 四态速记

  • 0:成功,stdout 有数据,唯一非错误终态
  • 2:用法错,参数层拦截,改命令行即可
  • 3-12:业务错,又分"确定性(9 个)"与"可重试(3 个:6/8/9)"
  • 70:内部异常,正常不该见,留 issue

📤 stderr 错误信封

失败时,CLI 把结构化错误以 JSON 信封写到 stderr,stdout 保持纯净(什么都不输出)。信封字段与 /api/errors 的 SDK 错误一一对应:

字段类型说明
okbool始终 false
commandstring来自哪条子命令(info / me / field / …)
argsobject调用入参快照,便于复现
error.codestring机器友好的错误码(如 INVALID_IP
error.messagestring人类可读的描述
error.sentinelstring对应的 SDK 哨兵错误(如 ErrInvalidIP
error.retryablebool是否值得重试
bash
$ ipapi info 999.1.1.1
# stdout: 空
# stderr:
json
{
  "ok": false,
  "command": "info",
  "args": { "ip": "999.1.1.1" },
  "error": {
    "code": "INVALID_IP",
    "message": "invalid IP address: 999.1.1.1",
    "sentinel": "ErrInvalidIP",
    "retryable": false
  }
}

⚠️ stdout / stderr 必须分流

ipapi CLI 的设计前提是"stdout 给管道,stderr 给人/日志"。脚本里若用 2>&1 合并,会把错误信封混进 stdout 数据流,破坏 jq 解析。判错用退出码,取数用 stdout,需要看错误细节时单独读 stderr:

bash
# 错误的做法:合并流后 jq 会因 stderr 信封而解析失败
ipapi info 999.1.1.1 2>&1 | jq .    # ❌

# 正确的做法:分流
ipapi info 8.8.8.8 2>err.json       # stdout 进终端,stderr 落 err.json
ipapi info 8.8.8.8 > out.json 2>err.json   # 各走各的

🐚 bash 判断示例

按退出码分流

bash
#!/usr/bin/env bash
set -uo pipefail

ip="${1:-8.8.8.8}"

if ipapi info "$ip" > "/tmp/${ip}.json" 2>"/tmp/${ip}.err"; then
  echo "✅ 查询成功,结果在 /tmp/${ip}.json"
  jq -r '.data | "\(.ip)\t\(.country)\t\(.org)"' "/tmp/${ip}.json"
else
  rc=$?
  case "$rc" in
    2)  echo "❌ 用法错误:检查参数" >&2 ;;
    3)  echo "❌ IP 非法:$ip" >&2 ;;
    4)  echo "❌ 字段名非法" >&2 ;;
    5)  echo "❌ 格式非法:-f 只能是 json/jsonp/xml/csv/yaml" >&2 ;;
    6)  echo "⚠️  限流,稍后重试" >&2 ;;
    7)  echo "❌ 保留 IP 段:$ip(私有/回环)" >&2 ;;
    8)  echo "⚠️  未找到,可重试" >&2 ;;
    9)  echo "⚠️  上游错误,可重试" >&2 ;;
    10) echo "❌ 方法不允许(应上报)" >&2 ;;
    11) echo "❌ API Key 无效" >&2 ;;
    12) echo "❌ 返回数据解析失败(应上报)" >&2 ;;
    70) echo "❌ 内部异常(应上报)" >&2 ;;
    *)  echo "❌ 未知退出码 $rc" >&2 ;;
  esac
  echo "── stderr 信封 ──" >&2
  cat "/tmp/${ip}.err" >&2
  exit "$rc"
fi

仅判"成功 / 可重试 / 不可重试"三态

很多场景下你不需要 13 个码全分,只要三态:

bash
ipapi info "$ip" > out.json 2>err.json
rc=$?

if [ $rc -eq 0 ]; then
  echo "成功"
elif [ $rc -eq 6 ] || [ $rc -eq 8 ] || [ $rc -eq 9 ]; then
  echo "可重试失败(码 $rc)"
else
  echo "不可重试失败(码 $rc)"
fi

取字段时按码降级

bash
# 拿不到 country 就降级为 UNKNOWN
country=$(ipapi field "$ip" country --human 2>/dev/null)
rc=$?
if [ $rc -ne 0 ]; then
  case "$rc" in
    6|8|9) country="RETRY_LATER" ;;
    *)     country="UNKNOWN" ;;
  esac
fi
echo "$ip -> $country"

📌 --human + 退出码是绝配

ipapi field <ip> <field> --human 成功时直出纯值一行,失败时退出码非 0 且 stderr 出信封。2>/dev/null 屏蔽信封、$? 判错——shell 取值的最佳姿态,比 info | jq 省流量又省解析。


🔁 退出码与 SDK 哨兵的对应

CLI 的退出码不是凭空发明的,它直接映射自 pkg/ipapi 的哨兵错误。换句话说,CLI 退出码 = SDK 错误的"shell 视角"。

退出码codeSDK 哨兵SDK 函数
3INVALID_IPErrInvalidIPValidateIP
4INVALID_FIELDErrInvalidField字段白名单校验
5INVALID_FORMATErrInvalidFormatValidateFormat
6RATE_LIMITEDErrRateLimitedIsRetryableError
7RESERVED_IPErrReservedIPhandleError
8NOT_FOUNDErrNotFoundIsRetryableError
9SERVER_ERRORErrServerErrorIsRetryableError
10METHOD_NOT_ALLOWEDErrMethodNotAllowedmapStatusCodeToError
11INVALID_KEYErrInvalidKeyhandleError
12UNEXPECTED_DATAErrUnexpectedDataJSON 解码层
🔗 270 没有 SDK 哨兵?

是的。2(USAGE)是 cobra 在参数解析阶段就拦截的,根本走不到 SDK;70(INTERNAL)是 CLI 兜底 recover 出来的未预期异常,也不对应某个具体哨兵。这两个码是 CLI 层专属,SDK 侧没有等价物。0 自然也没有——它就是"没错误"。

完整的哨兵错误定义、handleError 出口、IsRetryableError 判定逻辑,见 /api/errors


🧪 各命令会触发哪些码

不同子命令"容易踩到"的码不一样。下表帮你快速定位"这条命令失败最可能是什么码":

命令常见码不可能出现的码
info <ip>2 3 6 7 8 9 11 12 704 5 10(不带字段/格式参数)
me2 6 8 9 11 703 4 5 7 10 12(无 IP/字段/格式参数)
field <ip> <field>2 3 4 6 7 8 9 11 705 10 12
me-field <field>2 4 6 8 9 11 703 5 7 10 12
raw <ip> -f <fmt>2 3 5 6 7 8 9 11 704 10 12(直出原始字节,不解码)
me-raw -f <fmt>2 5 6 8 9 11 703 4 7 10 12
fields2其余(本地无网络)
version2其余(本地无网络)
completion <shell>2其余(本地生成)

📌 fields / version / completion 几乎不会失败

这三条是本地命令,不发起网络请求——除了参数错(2),不会出业务码。fields 列 28 字段、version 报版本、completion 生成补全脚本,全部离线完成。如果你在这三条上见到非 0/2 的码,大概率是 70(INTERNAL),请上报。


❓ 常见问题

为什么 70 而不是用更大的数字?

70 沿用了 BSD sysexits.hEX_SOFTWARE(70)的约定——"软件内部错误"。ipapi CLI 把 3-12 留给业务错误,70 作为"未预期"的兜底,符合 Unix 习惯。2 也来自 POSIX 约定(Shell Builtin / 用法错)。

退出码会随版本变化吗?

退出码是 CLI 与脚本之间的契约,不会轻易增删。新增业务错误会启用未占用的码(目前 113-69 未用),不会复用已分配的码。code / sentinel 字符串同样稳定。脚本可以放心硬编码这些码。

为什么 stdout 在失败时是空的?我看不到任何输出。

这是设计如此。失败时 stdout 不输出任何内容,错误信封只走 stderr——保证 ipapi ... | jq 这种管道在失败时不会把 JSON 信封混进数据流。需要看错误时,读 stderr 或加 2>err.json

--retries 会重试不可重试的码吗?

不会。SDK 的重试逻辑只在 IsRetryableError(err) 为真时触发,即仅 ErrRateLimited / ErrServerError / ErrNotFound 三类。USAGE / INVALID_IP 等不可重试错误,--retries 多大都不会重打。


下一步

对应 SDK 方法

ipapi CLI 是 pkg/ipapi SDK 的命令行封装。本页涉及的退出码与 SDK 错误处理方法对应关系:

CLI 概念SDK 方法 / 类型文档
退出码 → 哨兵映射哨兵错误集(ErrInvalidIP 等)/api/errors
retryable 判定IsRetryableError(err) bool/api/is-retryable
错误包装WrapError(op string, err error) error/api/wrap-error
自定义错误处理WithErrorHandler(fn)/api/with-error-handler
状态码 → 错误mapStatusCodeToError(内部)/api/errors

🔗 源码

CLI 的退出码映射逻辑见仓库 cmd/ipapi/ 目录(exitcode.go);SDK 侧的哨兵错误与 IsRetryableError 实现见 pkg/ipapi/errors.gopkg/ipapi/client.go

基于 MIT 许可证发布