系统学习 故障排查学习路径:从方法论到系统、网络、数据库与 K8s

unexpected-eof

读取响应时连接在预期结束前断开并报 unexpected EOF 的定位与修复

现象

Go 程序报 unexpected EOF、http: unexpected EOF reading trailer 或 JSON 解码时直接返回 unexpected EOF,curl 可能显示 transfer closed with outstanding read data remaining,TLS 场景则可能是 SSL_ERROR_SYSCALL。它表示读取方还在等待更多字节,连接却被关闭了。

Go 的 io.EOF 是正常结束:调用方已经读到约定的末尾。io.ErrUnexpectedEOF 则是在还需要数据时提前遇到 EOF,例如声明的 Content-Length 没读满、chunked 编码缺少结束块、TLS 记录或压缩流被截断。定位重点是“谁先关闭、关闭时响应体还差多少、关闭是 FIN 还是 RST”。

快速定位

先对比声明的长度和实际收到的字节数:

curl -v --http1.1 https://<host>/<path> -o /tmp/resp.bin
curl -sS -D - -o /dev/null https://<host>/<path> | grep -i -E 'content-length|transfer-encoding|content-encoding'
wc -c /tmp/resp.bin
gzip -t /tmp/resp.bin
timeout 10 openssl s_client -connect <host>:443 -servername <host> -quiet </dev/null >/tmp/tls-body.bin
ss -tin '( dport = :<port> or sport = :<port> )'
tcpdump -ni any -tttt -s 0 'host <host> and port <port>'
journalctl -u <service> --since '30 min ago' | grep -i -E 'panic|oom|shutdown|timeout|reset'
kubectl logs <pod> -n <ns> --previous --tail=100

如果直连后端正常、经过负载均衡或代理时失败,优先怀疑中间层的空闲超时、响应缓冲上限或后端过早关闭。curl -v 里看到 Content-Length 但下载字节数偏少,说明响应被截断;没有长度且使用 chunked,则检查是否缺少结束块。抓包时看 FIN 还是 RST:FIN 更像优雅关闭但关闭过早,RST 更像进程崩溃、连接被重置或中间设备主动中断。

代码里不要把截断当成空响应处理:

body, err := io.ReadAll(resp.Body)
if errors.Is(err, io.ErrUnexpectedEOF) {
    log.Printf("truncated body: got=%d want=%d", len(body), resp.ContentLength)
}
if resp.ContentLength >= 0 && int64(len(body)) != resp.ContentLength {
    return fmt.Errorf("short response: %d/%d", len(body), resp.ContentLength)
}

常见根因

  1. 服务进程崩溃、被 OOM 杀死或重启,响应只写了一半连接就消失。
  2. 应用发生 panic 或写入失败后提前关闭连接,但没有记录错误。
  3. 负载均衡、反向代理或网关的读超时短于后端生成响应的耗时,中间层先关闭连接。
  4. 应用声明的 Content-Length 大于实际写入的字节数,或 chunked 响应缺少终止块。
  5. 流式响应耗时很长,中间层按空闲超时断开,而服务端仍在继续写。
  6. TLS 连接没有发送 close_notify 就关闭,部分客户端将其视为截断。
  7. 客户端 context 被取消或 request body 读取中断,代理把它转换成不完整响应。
  8. HTTP/2 的流被重置或收到 GOAWAY,上层只看到响应体提前结束。
  9. 压缩流被截断,客户端解压时才发现数据不完整。
  10. MTU 黑洞或链路丢包导致连接重置,大响应比小响应更容易暴露。
  11. 文件在发送过程中被修改或截断,框架按旧长度发送。
  12. 服务端设置了会立即重置连接的关闭方式,发送缓冲区里的尾部数据被丢弃。

修复

相关命令

参考链接

读完自测

合上页面,先回答,再展开对照。

  1. io.EOF 和 io.ErrUnexpectedEOF 的区别是什么?
  2. 响应声明了 Content-Length,但客户端只收到一部分数据,常见原因有哪些?
  3. 幂等 GET 遇到 unexpected EOF 可以重试吗,POST 为什么不能直接照做?
答案
  1. io.EOF 表示按约定正常读到末尾;io.ErrUnexpectedEOF 表示还需要数据时连接提前结束,例如长度没读满、chunked 缺少结束块或压缩流被截断。
  2. 可能是服务进程崩溃或被 OOM 杀死、代理读超时先关闭、应用写出的字节数少于声明的长度、TLS 未正常关闭或链路重置;要对比直连与经代理的结果并查看两端日志。
  3. 幂等 GET 可以带退避和次数上限重试;POST 可能已经在服务端执行,盲目重试会重复写入,应先查结果、使用幂等键或由服务端提供去重机制。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令