DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Bash 中 curl 管道失败后继续执行,脚本却返回成功

Bash 中 curl 接 jq 后,请求失败可能被下游或最后一条命令的成功状态掩盖。用 pipefail 和显式状态累计,让多个请求继续执行,并在末尾统一返回失败;同时区分 curl 与 jq 的退出码。

脚本依次请求两个接口,把 curl 的输出交给 jq,最后打印结果。第一个请求已经失败,CI 却显示成功;加上 set -euo pipefail 后,又变成第一处错误就退出,第二个接口和汇总步骤都没有执行。

如果目标是“两个请求都尝试,最后只要有一个失败就返回非零”,需要同时处理管道状态与脚本最终状态。pipefail 负责让前面的命令失败能够反映到管道结果,脚本则要保存这些失败,最后显式退出。

下面使用 Bash、curl 和 jq,示例中的接口地址需要换成实际的只读接口。不要用 sh script.sh 启动这份 Bash 脚本,也不要直接把脚本 source 到交互会话里。

两处成功状态可能掩盖请求失败

常见写法是:

curl --fail --silent --show-error 'https://api.example.com/report/one' |
  jq -S . > output1.json
cat output1.json

未启用 pipefail 时,管道退出状态通常取最后一个命令,也就是 jq 的状态。curl 失败不意味着下游一定失败;例如普通 jq . 收到空输入,不能据此认定请求成功。

即使开启了 pipefail,如果后面继续执行成功的 cat,又没有保存前面的状态并显式 exit,脚本最终仍可能返回成功。文件存在也不能代替状态检查:重定向可以先创建或截断文件,请求失败后留下的可能是空文件或不完整内容。

--fail 与 pipefail 分属不同层次。curl 默认不把 HTTP 错误状态直接当成传输失败;--fail 让通常的 HTTP 400 及以上响应返回错误码 22,并抑制响应体。认证过程等存在例外,业务上的失败也未必使用 HTTP 错误状态,因此仍要按接口约定判断响应内容。

逐项捕获失败,最后统一返回 1

下面是完整的两请求示例。先在新的工作目录使用,避免覆盖已有的 output1.json、output2.json。它只在对应管道成功时打印该文件,失败文件不能交给后续任务当作有效数据。

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

url1='https://api.example.com/report/one'
url2='https://api.example.com/report/two'
failed=0

if curl --fail --silent --show-error "$url1" |
   jq -S . > output1.json; then
  if ! cat output1.json; then
    failed=1
  fi
else
  rc=$?
  printf 'request 1 pipeline failed: %s\n' "$rc" >&2
  failed=1
fi

if curl --fail --silent --show-error "$url2" |
   jq -S . > output2.json; then
  if ! cat output2.json; then
    failed=1
  fi
else
  rc=$?
  printf 'request 2 pipeline failed: %s\n' "$rc" >&2
  failed=1
fi

printf 'request processing finished; failed=%s\n' "$failed" >&2
exit "$failed"

每条管道直接作为 if 条件。失败后进入 else,第一条语句就保存 $?,随后记录错误并继续。只要任一管道或结果打印失败,failed 就保持为 1;后面另一个请求成功也不会清掉这个状态。

本例没有开启 set -e。若现有脚本仍保留它,直接放在 if 条件中的这两条管道也属于不会因失败而立即退出的情况。不过,脚本里其他未处理的失败仍可能终止执行,不能据此承诺整个脚本遇到任何错误都能继续。

不要把失败分支写成 if ! curl ... | jq ...; then rc=$? 来获取原始失败码。! 已经把条件状态取反,进入分支时读到的可能是 0。上面的 if ...; then ...; else 保留了失败管道的状态。

要区分 curl 与 jq 的失败,保存 PIPESTATUS

pipefail 返回的是管道中最右侧非零状态,不一定是 curl 的原始错误码。若 curl 与 jq 都失败,只看 $? 无法准确区分两者。

在单独的诊断脚本中,可以紧接管道保存 Bash 的 PIPESTATUS 数组:

#!/usr/bin/env bash
set +e
set -o pipefail

curl --fail --silent --show-error 'https://api.example.com/report/one' |
  jq -S . > diagnostic.json
stages=("${PIPESTATUS[@]}")
printf 'curl=%s jq=%s\n' "${stages[0]}" "${stages[1]}"

数组下标 0 对应 curl,1 对应 jq。保存之前不能先加 echo、rc=$? 或其他命令,因为执行简单命令也会更新 PIPESTATUS。这段脚本只用于查看各阶段状态,末尾的打印结果不代表接口成功;正式任务仍应使用前面的累计失败逻辑。

如果接口约定必须返回一个非空的 JSON 结果,可评估 jq -e:最后输出为 false 或 null 时返回 1,没有有效结果时返回 4。但这些值在某些接口里可能合法,不能只为了让 CI 变红就全局替换。JSON 语法正确,也不等于业务结果符合要求。

用受控失败验证脚本的最终结果

在测试环境分别安排“第一个失败、第二个成功”“第一个成功、第二个失败”和“两者成功”,再检查脚本退出码与日志。前两种情况都应执行到汇总并返回 1,两者成功应返回 0;还应确认错误响应、无效 JSON 和不可写输出文件能够被识别。不要在生产环境故意制造接口故障。

脚本内部打印“完成”与成功退出是两回事。调用方应读取脚本的退出码;若调用方又用 bash script.sh | tee run.log 包了一层管道,它也需要正确处理那一层的状态。

修改前保留原脚本和输出文件,回退时恢复脚本即可,不需要删除历史结果。失败运行留下的文件应与有效结果分开处理。带认证信息的请求不要随手开启 set -x,它会打印展开后的参数,可能把凭据写入 CI 日志。