grpc-gateway默认不捕获panic,需在runtime.servemux上手动添加recover中间件;它仅拦截http层panic(如参数解析、middleware崩溃),不处理grpc server端panic,后者需通过grpc拦截器单独兜底。

gRPC网关(grpc-gateway)默认不捕获panic,必须手动加recover
grpc-gateway 本质是 HTTP 反向代理层,它把 REST 请求转成 gRPC 调用,但底层 http.ServeHTTP 的 panic 是透传的——不会被 http.DefaultServeMux 或 grpc-gateway 自身捕获。一旦后端 gRPC handler 或中间件 panic,整个 HTTP 连接会直接断开,返回 500 且无日志(除非你启用了 http.Server.ErrorLog)。这不是 bug,是设计使然:它不希望掩盖业务层未处理的崩溃。
在 grpc-gateway 的 runtime.NewServeMux 上套一层 recover 中间件
最稳妥的位置是在 runtime.NewServeMux() 实例上做包装,而不是改 gRPC server 或 handler。因为 gateway 的请求生命周期完全走 HTTP 路由,所有转换后的调用都经过这个 mux 的 ServeHTTP 方法。
实操建议:
- 不要用
http.WrapHandler或第三方中间件库(如gorilla/handlers),它们可能绕过 gateway 内部的 context 传递或 metadata 注入 - 直接 wrap
*runtime.ServeMux的ServeHTTP方法,用 defer + recover 拦截 - recover 后必须显式写 response,否则连接挂起;推荐统一返回
500 Internal Server Error和简短 JSON 错误体 - 务必记录 panic 的 stack trace(用
debug.Stack()),否则线上无法定位
示例代码片段:
func withRecover(next *runtime.ServeMux) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC in grpc-gateway: %v\n%v", r, debug.Stack())
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
}
}()
next.ServeHTTP(w, r)
})
}
<p>// 启动时
mux := runtime.NewServeMux()
// ... 注册所有 gRPC services 到 mux
http.ListenAndServe(":8080", withRecover(mux))
</p>
注意 recover 不会捕获 gRPC server 端的 panic
这是常见误解。grpc-gateway 的 recover 只管 HTTP 层的 panic,比如:
诊断并恢复通过 SSH 隧道连接的 OpenClaw 节点。用于解决配对必需错误、隧道冲突、远程端点错误以及 SSH 目标配置错误等问题。
- REST 参数解析失败导致的 panic(如自定义
runtime.WithMarshalerOption里解码出错) - gateway middleware(如 auth、logging)中 panic
-
runtime.WithIncomingHeaderMatcher回调里 panic
但它**完全不管** gRPC server 端(即你 RegisterXXXServer 的那个 server)里的 panic。那些 panic 会被 gRPC 的 Server 捕获并转为 status.Error,最终由 gateway 映射为 5xx 响应——但不会触发你上面写的 recover。
所以如果你要统一兜底,得在两处加:
- HTTP 层:用上面的
withRecover - gRPC server 层:用
grpc.UnaryInterceptor+ recover(参考grpc_recovery库的实现)
别在 runtime.ServeMux 注册前 panic,recover 失效
如果 panic 发生在 mux.Handle(...) 期间(比如 proto 注册时字段校验失败、marshaler 初始化异常),那它根本不会走到 ServeHTTP 流程,recover 完全没机会执行。这类 panic 必须在 main() 或初始化阶段用顶层 defer 捕获,或者更早——用 go build -gcflags="-e" 提前暴露编译期问题。
典型易踩坑点:
- 在
runtime.WithMarshalerOption传入的 marshaler 里写了 panic-prone 逻辑(比如强制类型断言未检查) - 用
runtime.WithMetadata回调访问了 nil context.Value - proto 文件生成的
RegisterXXXHandlerFromEndpoint调用时 endpoint 不可达,但该函数本身不 panic —— 真正 panic 往往在后续第一次请求时才触发
真正稳定的 recover 只覆盖「已进入 HTTP 处理循环」的路径。其余初始化阶段的 panic,得靠测试和防御性编程提前掐掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










