curl 与 HTTPie 协议诊断和可审计 Smoke 手册
浏览器报错,复制成 curl 却返回成功
前端页面提示“网络错误”,从 DevTools 复制出的 curl 却拿到 200。这不是矛盾:curl 没有浏览器的同源策略、SameSite Cookie、Service Worker 和页面状态。反过来,浏览器能访问、CI 的 curl 却报证书错误,也可能是开发机信任了企业根证书,而 runner 没有。
命令行探针的价值,是把一次请求拆成可判定的阶段:URL 解析,DNS,代理选择,TCP/QUIC 连接,TLS 握手,HTTP 交换,重定向,最后才是 JSON 业务语义。curl 更适合精细控制协议和稳定脚本;HTTPie 的请求语法与结构化输出更适合人工阅读。两者都不会自动证明业务正确,也不能代替浏览器取证或完整契约测试。
先选一个只读测试接口和低权限测试身份。确认请求是否允许离开当前网络、企业代理和 CA 从哪里取得、服务端能否按 request id 查日志。终端历史、进程参数、CI 日志和 trace 文件都可能暴露凭证,第一次带认证头之前就要决定 Secret 的注入与销毁方式。
安装后先识别你实际运行的构建
Windows 10/11 默认带有 Microsoft 分发的 curl.exe,其 TLS 后端和编译特性可能与 curl 官方 Windows 构建不同;PowerShell 中还应显式写 curl.exe,避免旧环境里的别名歧义。官方说明见 Windows 内置 curl。
Get-Command curl.exe
curl.exe --versionmacOS 和 Linux 通常也预装 curl,但发行版版本、TLS 后端、HTTP/2/HTTP/3、brotli、zstd 和证书存储支持并不一致:
command -v curl
curl --version只有确实需要较新参数或特定协议特性时,才使用系统包管理器或 curl 官方下载页 升级,并在 CI 镜像中固定版本。curl 官方当前稳定源码发布为 8.21.0,但操作系统自带版本通常更旧,支持能力仍应以本机 curl --version 为准。脚本若使用 --fail-with-body、--json 等较新选项,应在启动时检查版本或提供兼容写法,不能假设所有 runner 与开发机相同。
HTTPie CLI 当前稳定版为 3.2.4,常用安装入口包括受隔离 Python 环境、Homebrew 和发行版包。团队应避免把依赖灌进系统 Python;开发机可使用 pipx,CI 则固定补丁版本:
pipx install 'httpie==3.2.4'
http --versionbrew install httpie安装与认证插件会改变 HTTPie 的行为和供应链面,CI 镜像中应同时锁定 HTTPie 与插件版本。参数语义以 HTTPie CLI 文档 为准。
第一次请求先观察,不急着加认证
用不带 Secret 的健康检查建立基线:
curl --silent --show-error \
--connect-timeout 3 \
--max-time 10 \
--dump-header /tmp/health.headers \
--output /tmp/health.body \
--write-out '%{http_code} %{remote_ip} %{time_connect} %{time_appconnect} %{time_total}\n' \
'https://api.example.com/health'--connect-timeout 限制连接阶段,包含 DNS、代理连接和建立协议连接所需时间;--max-time 限制整个传输。time_connect 与 time_appconnect 能帮助判断慢在 TCP 还是 TLS,但它们不是完整分布式追踪。响应头和 body 分文件保存,便于先审查状态与 content type,再决定是否解析正文。
HTTPie 的人工诊断写法更短:
http --headers --timeout=10 GET https://api.example.com/health进入脚本后必须补 --ignore-stdin。HTTPie 在非交互环境可能把重定向的标准输入当作请求体并等待输入,看起来像“命令卡死”;官方的 HTTPie scripting 指南 明确建议脚本默认禁用 stdin 读取。
三组正反实验建立故障证据
DNS 与连接失败
把主机改为保留的无效域名:
curl --connect-timeout 2 --max-time 4 https://does-not-exist.invalid/health
echo "$?"预期 curl 返回非零,典型退出码 6 表示无法解析主机;服务端不会出现 request id。若返回代理生成的 HTML 错误页,说明 DNS 可能由代理代解析,诊断要转向代理链路。
再把端口改为一个确定未监听的测试端口。典型退出码 7 表示无法连接主机或代理。DNS 已有结果并不代表端口、安全组或进程可达。
HTTP 失败不能静默变绿
不加失败选项请求一个确定返回 404 的路径:
curl --silent --output /dev/null https://api.example.com/definitely-missing
echo "$?"预期 curl 可能退出 0,因为 HTTP 响应已经成功传输。加入状态门禁后再跑:
curl --silent --show-error --fail-with-body \
--max-time 10 \
https://api.example.com/definitely-missing
echo "$?"预期退出码 22,并保留错误响应体。curl 官方错误码说明见 libcurl error codes。HTTPie 对应写法是:
http --check-status --ignore-stdin --timeout=10 \
GET https://api.example.com/definitely-missing
echo "$?"启用 --check-status 后,HTTPie 对未跟随的 3xx、4xx、5xx 分别使用退出状态 3、4、5。团队脚本不要把 curl 与 HTTPie 的退出码表混用。
超时要区分连接慢和响应慢
连接一个不可达测试地址可以触发连接超时;请求测试服务提供的延迟端点可以触发总超时:
curl --connect-timeout 1 --max-time 2 \
https://api.example.com/test/delay/5
echo "$?"典型退出码 28 只说明达到配置的超时条件。要结合 time_connect、time_starttransfer、代理日志和服务端 request id 判断是 DNS/连接、TLS、首字节还是响应体阶段。不要通过盲目把总超时改成几分钟来“修复”流水线;这只会扩大排队和占用成本。
JSON、认证和 shell 引号必须可移植
Bash 中常见的单引号 JSON 在 PowerShell 与 cmd.exe 中不能原样照搬。跨平台 smoke 最稳妥的办法是把非敏感请求体放入文件:
{
"name": "smoke-probe"
}Bash:
curl --fail-with-body --max-time 10 \
--header 'Content-Type: application/json' \
--data-binary @request.json \
"$API_BASE_URL/api/probes"PowerShell:
curl.exe --fail-with-body --max-time 10 `
--header "Content-Type: application/json" `
--data-binary "@request.json" `
"$env:API_BASE_URL/api/probes"HTTPie 可以用字段语法构造 JSON,字符串、数字和布尔值的操作符不同。CI 中更适合显式输入原始文件,避免工具升级或字段类型误判改变 body。
Secret 从 CI Secret 或本地密码管理器注入为环境变量,但环境变量仍可能被子进程、debug 输出或崩溃转储读取。关闭 set -x、PowerShell transcript 和 CI step debug,不把真实 token 放入命令示例:
: "${API_TOKEN:?missing API_TOKEN}"
printf 'Authorization: Bearer %s\n' "$API_TOKEN" | \
curl --fail-with-body --max-time 10 \
--header @- \
"$API_BASE_URL/api/me"这里让 shell 内建的 printf 通过标准输入提供请求头,避免 token 展开后直接出现在 curl 的参数列表中;仍须关闭 xtrace,因为调试跟踪会打印展开后的参数。带请求体时不要同时占用 stdin,可改用权限为 600 的临时 header 文件,并用 trap 在退出时删除。
对长期机器身份,优先短时令牌和工作负载身份;静态 token 应限制 scope、环境和有效期。.netrc、HTTPie session、Cookie jar、客户端证书和私钥必须权限收紧并排除出 Git。
代理证书与服务证书是两条 TLS 链
环境变量 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 和 NO_PROXY 可能来自登录 shell、系统服务或 CI runner。先显示变量名是否存在,不打印含账号密码的完整代理 URL:
env | sed -n '/^[A-Za-z_]*_PROXY=/s/=.*/=<set>/p'curl 可用 --proxy 显式指定代理,用 --noproxy 做受控绕过实验。HTTPS 代理到客户端、目标 HTTPS 服务到客户端是两条独立验证链:目标 CA 使用 --cacert,代理 CA 使用 --proxy-cacert。curl 官方说明见 SSL CA Certificates。
curl --proxy https://proxy.example.com:8443 \
--proxy-cacert "$PROXY_CA_FILE" \
--cacert "$SERVICE_CA_FILE" \
--connect-timeout 3 --max-time 10 \
https://api.example.com/healthmTLS 还需要客户端证书和私钥:
curl --cacert "$SERVICE_CA_FILE" \
--cert "$CLIENT_CERT_FILE" \
--key "$CLIENT_KEY_FILE" \
https://api.example.com/health-k / --insecure 的正当用途只有一次性对照:不开启时报退出码 60,开启后能建立连接,说明验证链或主机名存在问题。它不能证明服务器可信,更不能进入脚本默认值。修复应是部署正确证书链、导入受信 CA,或修正 SNI/主机名。
HTTPie 使用按目标协议区分的 --proxy=<protocol>:<proxy-url>、--verify、--cert 与 --cert-key 表达同类配置,例如:
http --proxy=https:http://proxy.example.com:8080 \
--verify="$SERVICE_CA_FILE" \
--cert="$CLIENT_CERT_FILE" \
--cert-key="$CLIENT_KEY_FILE" \
GET https://api.example.com/health代理 URL 的 http:// 表示到代理本身的连接方式,左侧的 https: 表示它处理 HTTPS 目标请求,两者不能混为一谈。不要假设 HTTPie、系统 Python、curl Schannel/OpenSSL 和浏览器使用同一个信任库;本机结果不一致时先记录各自版本与 TLS 后端。
trace 要能解释问题,也要能安全销毁
curl 的 --verbose 适合快速看连接与头部,--trace-ascii 能记录更完整的传输细节。它们可能包含 Authorization、Cookie、查询参数和 body:
trace_file="$(mktemp)"
chmod 600 "$trace_file"
curl --trace-ascii "$trace_file" \
--trace-time \
--connect-timeout 3 --max-time 10 \
https://api.example.com/health仅在受控目录生成 trace,完成诊断后提取协议版本、握手失败点、状态码和 request id,脱敏副本再进入问题单;原文件按安全策略删除。不要在带真实 token 的请求上直接运行 trace 后上传整个文件。
HTTPie 的 --verbose 会展示请求和响应元数据,双 --verbose 还会增加计时信息。session 会持久化 Cookie 和头部,适合连续人工调试,却不适合作为可随意归档的日志。问题单证据应是可重放的脱敏命令、工具版本、退出码、最小响应头和服务端 request id,而不是终端整屏截图。
把 smoke 写成审计友好的项目入口
一个 Bash smoke 要同时处理状态码、超时、业务断言和临时文件清理:
#!/usr/bin/env bash
set -euo pipefail
: "${API_BASE_URL:?missing API_BASE_URL}"
body_file="$(mktemp)"
header_file="$(mktemp)"
cleanup() { rm -f "$body_file" "$header_file"; }
trap cleanup EXIT
http_code="$({
curl --silent --show-error --fail-with-body \
--connect-timeout 3 --max-time 10 \
--dump-header "$header_file" \
--output "$body_file" \
--write-out '%{http_code}' \
"$API_BASE_URL/health"
} || { rc=$?; echo "transport_or_http_failure rc=$rc" >&2; exit "$rc"; })"
test "$http_code" = "200"
jq -e '.status == "UP"' "$body_file" >/dev/null
grep -i '^x-request-id:' "$header_file" | head -n 1这里的 curl 0 只通过传输与 HTTP 门禁,jq 再判断业务语义。若接口没有稳定 JSON schema,应检查最小且不会泄露数据的字段,不能 grep 一段易变文案。
HTTPie 的最小脚本应显式处理 stdin 与状态:
http --check-status --ignore-stdin --timeout=10 \
--print=hb \
GET "$API_BASE_URL/health"CI 入口固定工具版本和脚本路径,Secret 由 job 权限注入,输出只保留 Git 提交、环境代号、退出状态、耗时和 request id。PR smoke 只跑只读与幂等请求;带写操作的环境回归使用隔离租户和幂等键,并设置并发上限,防止重试放大副作用。
PowerShell 团队应维护原生 .ps1,检查 $LASTEXITCODE,不要把 Bash 的 set -euo pipefail 和 $? 语义机械翻译。Windows PowerShell 管道还可能把文本编码改为 UTF-16;向 curl 发送文件时使用 --data-binary @file,并明确文件编码。跨 shell 共享的是输入文件、预期状态和证据格式,不必强求共享同一个脚本文件。
失败地图从退出码走向责任边界
| 证据 | 常见位置 | 下一步 |
|---|---|---|
curl 5 | 代理域名解析 | 检查代理地址、DNS、VPN |
curl 6 | 目标域名解析 | 检查 DNS、hosts、代理解析方式 |
curl 7 | TCP/代理连接 | 检查端口、进程、网关、安全策略 |
curl 22 | HTTP 400+ 且启用 fail | 保留脱敏 body,按 request id 查应用日志 |
curl 28 | 达到超时条件 | 对比连接、TLS、首字节与总耗时 |
curl 35 | TLS 握手 | 检查协议、SNI、客户端证书和中间设备 |
curl 60 | 服务端证书验证 | 修复 CA、证书链或主机名 |
HTTPie 2 | 请求超时 | 检查 --timeout 与网络阶段 |
HTTPie 3/4/5 | 重定向/客户端错误/服务端错误 | 检查 follow 策略、认证和应用日志 |
退出 0 但字段错误 | 业务语义 | 用 jq 或契约测试断言 schema 与业务码 |
一个退出码不能独立成为根因。比如 28 可能由代理无响应、TLS 卡住、服务端迟迟不发首字节或下载过慢造成;必须和计时、request id、代理日志及服务端日志组合。
清理、回滚和长期维护
排障结束后删除 body、header、trace、Cookie jar、HTTPie session、临时 CA 与客户端证书副本,清理终端历史和 CI artifact,并撤销临时 token。Secret 若出现在命令行参数、日志或 Git 中,按已泄露处理并轮换,不能只删除那一行。
升级导致 smoke 失败时,先比较 curl --version 或 http --version、TLS 后端和实际参数帮助,再用锁定的旧镜像复跑同一只读请求。确认是工具行为变化后,回退 CI 镜像或依赖锁,并保留脱敏的正反输出;不要把 --insecure、取消超时或忽略退出码作为回滚方案。
工具本身几乎不产生云资源费用,真正的容量成本来自流水线分钟、并发连接、第三方 API 计费、网关限流、测试数据和日志留存。团队应分层运行:每次提交执行少量 smoke,合并后执行契约与回归,按需人工 trace。为 smoke 设置总时限、重试上限和并发上限;只有幂等请求才能自动重试,并记录重试次数,避免一次下游抖动被放大成流量事故。
定期删除废弃端点与身份、轮换 CA 和客户端证书、更新固定版本、审查新增参数是否受旧 runner 支持,并抽查日志是否仍然脱敏。curl 适合成为最小可移植探针,HTTPie 适合成为可读的交互客户端;当请求需要复杂数据驱动、共享环境、断言编排和报告治理时,应升级到 collection 或契约测试工具,而不是把 shell 脚本扩成难以审计的测试框架。
