HTTPS检查突然报curl: (28) Operation timed out,日志前面却写着Connected。证书刚换过,很容易顺手把锅甩给它。但TCP接通不是TLS握手完成,TLS完成也不是HTTP响应到齐。先找出超时发生在哪一段,再决定查网络、握手还是应用,别让重签证书变成安慰剂。
一、28说明超时,不直接说明证书坏了
curl错误28表示操作超时。它并不指定唯一根因:名称解析、连接、TLS握手、等待响应或传输过程都可能用尽预算。证书或对端身份验证失败通常对应60,但最终错误取决于先发生的失败;如果握手还没收到足够数据,不能要求工具先给你一个证书结论。
本文讨论单次、新建、直连的HTTPS请求,不带重试、跳转与连接复用。实验使用本机curl 7.61.1、OpenSSL 1.1.1k和回环临时服务,未测试公网链路,未改生产配置。先记录实际版本,不能把网上最新版选项当作本机已有。
bash
curl --version
二、connect-timeout不只管TCP
官方手册将连接阶段定义为DNS查询及所需TCP、TLS等握手完成之前。对这里的HTTPS请求,connect-timeout覆盖TLS握手;max-time限制整次传输,连接阶段也包含在内。两个值不是可以相加的额度,也不是先用完连接预算,再另领一份总预算。

图只画适用范围,不表示每段必然耗时相同,也不是HTTP/3或代理链路的完整时序。TCP已建立而TLS停住时,connect-timeout仍可能到期;TLS完成以后慢慢等HTTP,连接超时就不是这段等待的截止线。
三、保留阶段计时,不只看最后一句
下面是诊断模板,example.com须替换为获准测试的目标。0.5秒和2秒用于说明,不是生产推荐阈值。-q必须作为第一个选项用于忽略默认配置文件;它不清空所有环境变量。--noproxy表示本次直连,--http1.1让协议边界更容易观察;必要代理环境不要照抄直连假设。
bash
curl -q --noproxy '*' --http1.1 -sS \
--connect-timeout 0.5 --max-time 2 \
-o /dev/null \
-w 'tcp=%{time_connect} tls=%{time_appconnect} first=%{time_starttransfer} total=%{time_total} http=%{http_code}\n' \
https://example.com/
time_connect、time_appconnect、time_starttransfer与time_total是从开始计算的累计时刻,不是四段互不重叠的耗时。对本例可用TLS完成时刻减TCP完成时刻观察握手区间,但有重定向、代理、复用时不能照搬这套减法。
字段为0可能表示对应阶段没有完成,不能仅凭它断定"TLS耗时为零"。http_code为000也不是服务器发送的HTTP状态码;它说明这里没有取得可报告的响应码。结合stderr和服务端事件,才能定位到具体阶段。
四、用两种停顿拆开同一个28
隔离对照一:服务接收TCP,但不回应TLS。客户端记录到TCP完成,TLS完成时间仍为0,在约0.5秒报28;服务端没有HTTP请求事件。这里连接已经"接上",连接阶段却没有完成。
隔离对照二:临时测试CA受信、名称正确、TLS正常完成,服务读取HTTP请求后暂不返回响应。同一命令在约2秒报28,TLS计时非零、尚无首字节。只盯着28看不出区别,服务端已收到请求这个证据很重要。
| 隔离条件 | 退出码 | 观察重点 |
|---|---|---|
| TCP接受后TLS停顿 | 28 | 连接阶段预算到期 |
| TLS完成后HTTP停顿 | 28 | 总传输预算到期 |
| 快速响应且信任正确 | 0 | 仍需核对HTTP与业务 |
| 正常握手但测试CA不受信 | 60 | 没有进入HTTP请求 |
五、总超时更短时,谁先到就先停
把两个值交换,可以检查"连接超时更长就一定能等更久"的误解。TLS停顿夹具在总预算约0.5秒时结束,不会因为连接允许2秒就继续等。
bash
curl -q --noproxy '*' --http1.1 -sS \
--connect-timeout 2 --max-time 0.5 \
-o /dev/null -w 'total=%{time_total} http=%{http_code}\n' \
https://example.com/
这些近似时间不是硬实时承诺。系统调度、解析器与运行环境都会影响观测,应保留实际值而不是断言小数点后完全相等。本文不启用--retry;启用重试后max-time会在重试时重置,整个任务还需要独立重试预算和调用次数上限。
六、把退出码与证据一起留下
把下面保存为check-timeout.sh,执行bash check-timeout.sh https://example.com/。脚本保留本次日志目录,TMPDIR须存在且可写,事后按本机保留策略清理。它不输出私钥或认证头;实际URL仍应避免携带Token与个人资料。
bash
#!/usr/bin/env bash
set -u
[[ $# == 1 ]] || exit 2
umask 077
work=$(mktemp -d "${TMPDIR:-.}/https-timeout.XXXXXX") || exit 2
curl -q --noproxy '*' --http1.1 -sS \
--connect-timeout 2 --max-time 5 \
-o /dev/null \
-w 'tcp=%{time_connect} tls=%{time_appconnect} first=%{time_starttransfer} total=%{time_total} http=%{http_code}\n' \
"$1" >"$work/timing.txt" 2>"$work/error.txt"
rc=$?
printf 'curl_exit=%s evidence=%s\n' "$rc" "$work"
exit "$rc"
立即保存curl退出码,别先echo再取$?。这里不用set -e,因为失败后还要记录结果;也不接tee管道,避免误拿日志程序的退出码。本轮已从最终HTML提取四块命令,在回环夹具执行正常、TLS停顿、HTTP停顿与不受信对照;临时私钥、监听和测试日志已清理。
七、修复按层做,验收不能只延长时间
- 保留版本、目标、超时参数、退出码及采样时间。
- 握手前停住,结合解析、网络与TLS端日志查路径,不凭Connected放行。
- 握手后停住,核对应用是否收到请求、首字节和响应体进度。
- 证书失败按链、名称与时间修复,不使用-k绕过,也不把调大超时当作信任修复。
- 最终重新验证TLS、HTTP状态及业务内容;0退出码本身不是业务成功。
客户端超时只是停止等待,不保证服务端没有执行请求。涉及写入时,先核对服务端结果和业务幂等设计,再决定重试;诊断脚本不能证明回滚成功。