← 中文资源

简体中文版本

Go HTTP handler panic 导致连接被关闭

通过在中间件中 recover、记录堆栈并返回受控的 500 响应,修复会中断请求的 Go net/http panic。

阅读英文原文

症状

遇到“Go HTTP handler panic 导致连接被关闭”时靠猜测只会浪费时间。请按证据排查:先复现症状,再执行针对性诊断,定位根因后修复,并用明确检查验证结果。

可观察到的症状包括:

  • 客户端收到 “empty reply from server” 而非 500
  • 服务端日志有 panic 堆栈,客户端却没收到响应体
  • 某端点间歇性出现 ECONNRESET

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

curl -i localhost:8080/endpoint; grep -n "panic:" server.log

堆栈帧会指出 panic 的确切处理器行,即真正缺陷所在。

步骤 2

mux := http.NewServeMux(); handler := recoverMiddleware(mux); http.ListenAndServe(":8080", handler)

recover 把被丢弃的连接变为客户端可处理的受控 500。

常见诱因(按出现频率排序):

  • 处理器 panic,net/http 内部恢复但仍中断响应
  • 某边界路径上处理器对 nil map 或切片索引写入
  • 下游调用返回 nil 却解引用了方法

根因

net/http 会 recover 发生 panic 的处理器以保护服务器,但处理器在响应中途 panic 后该回复即被中断;客户端看到的连接被关闭而非响应,也不会产生错误体。

修复

用中间件包裹 mux,recover 后记录带请求上下文的堆栈,并在响应头尚未发送时写入 500 JSON 错误。

注意:recover 不应在测试中吞掉 panic;保留一种会重新抛出的测试模式,让缺陷可见。

func recoverMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				log.Printf("panic on %s %s: %v\n%s", r.Method, r.URL.Path, rec, debug.Stack())
				http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

验证

触发此前 panic 的端点,确认返回带 JSON 体的 500 并记录堆栈,且进程保持存活。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。