Gin源码分析 – 中间件 – Recovery
1 介绍
Recovery中间件,能够捕获HTTP请求处理过程中产生的所有panic,并返回500错误。
2 使用
下面给出两个例子进行说明。第一个例子没有使用Recovery中间件,第二个例子使用了Recovery中间件,看看效果。
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.New() r.GET("panic", func(c *gin.Context) { panic("我panic啦") }) r.Run(":8080") }
上面这个例子使用gin.New创建Engine,没有注册 Recovery中间件,运行服务后,使用 httpie发送了一个/panic请求,发现服务端和客户端都出现了异常,在服务端直接打印的异常堆栈;在客户端则直接出现了连接异常。
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("panic", func(c *gin.Context) { panic("我panic啦") }) r.Run(":8080") }
这个例子使用gin.Default()创建了Engine,在内部注册了Recovery中间件,Recovery中间件捕获了panic,以更好的方式显示了异常堆栈,并且给客户端返回了HTTP 500错误。
3 源码分析
3.1 主要接口
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. func Recovery() HandlerFunc { return RecoveryWithWriter(DefaultErrorWriter) } //CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it. func CustomRecovery(handle RecoveryFunc) HandlerFunc { return RecoveryWithWriter(DefaultErrorWriter, handle) } // RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one. func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc { if len(recovery) > 0 { return CustomRecoveryWithWriter(out, recovery[0]) } return CustomRecoveryWithWriter(out, defaultHandleRecovery) } // CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it. func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { }
从代码可以看出,存在多个Recovery的变种:
(1)func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc,是最底层的形式,提供Writer,RecoveryFunc两个参数,分别定义了异常的显示输出和具体的恢复方法;
(2)func RecoveryWithWriter(out io.Writer, recovery …RecoveryFunc) HandlerFunc,提供了Writer参数和…RecoveryFunc,如果存在该函数则直接使用该函数,如果没有定义这个方法则使用defaultHandleRecovery这个方法;
(3) func CustomRecovery(handle RecoveryFunc) HandlerFunc,使用默认的DefaultErrorWriter作为Writer,使用参数RecoveryFunc作为恢复方法;
(4)func Recovery() HandlerFunc,使用DefaultErrorWriter和defaultHandleRecovery这两个默认方法。
3.2 DefaultErrorWriter
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors var DefaultErrorWriter io.Writer = os.Stderr
3.3 defaultHandleRecovery
func defaultHandleRecovery(c *Context, err interface{}) { c.AbortWithStatus(http.StatusInternalServerError) } const( StatusInternalServerError 500 )
3.4 CustomRecoveryWithWriter
这个函数是整个Recovery中间件的核心方法,代码行数略多,整体过程如下:
(1)创建日志,创建了一个log.LstdFlags的Logger,字体颜色是红色
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it. func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { var logger *log.Logger if out != nil { logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) } ... }
(2) 返回处理函数,整个处理方法写在一个defer中,从而确保后面c.Next()方法中的所有panic都可以被捕获到,而c.Next()实际上会处理后续的所有中间件和业务处理器。
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it. func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFun ... return func(c *Context) { defer func() { ... }() c.Next() } }
(3)捕获函数的整体代码如下
defer func() { // (1)获取异常 if err := recover(); err != nil { // (2) // Check for a broken connection, as it is not really a // condition that warrants a panic stack trace. var brokenPipe bool if ne, ok := err.(*net.OpError); ok { if se, ok := ne.Err.(*os.SyscallError); ok { if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { brokenPipe = true } } } if logger != nil { // (3) stack := stack(3) // (4) httpRequest, _ := httputil.DumpRequest(c.Request, false) // (5) headers := strings.Split(string(httpRequest), "\r\n") for idx, header := range headers { current := strings.Split(header, ":") if current[0] == "Authorization" { headers[idx] = current[0] + ": *" } } headersToStr := strings.Join(headers, "\r\n") // (6) if brokenPipe { logger.Printf("%s\n%s%s", err, headersToStr, reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), headersToStr, err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) } } if brokenPipe { // If the connection is dead, we can't write a status to it. c.Error(err.(error)) // nolint: errcheck c.Abort() } else { handle(c, err) } } }()
对于整个过程分析如下:
(1)首先通过recover()获取panic;
(2)判断是否错误是因为客户端断开连接造成的如果是则brokenPipe设置为true;
(3)调用stack获取异常堆栈,在这个方法调用了runtime.Caller(i)以及自定义的source函数、function函数和timeFormat函数;
(4)调用httputil.DumpRequest,获取请求头;
(5)获取HTTP Request Header,同时为了安全的原因,隐藏了Authorization的相关信息;
(6)分情况输出异常信息;
(7)如果是异常则执行handle,默认是defaultHandleRecovery,这个方法返回HTTP 500;如果是网络原因则调用Abort
4 总结
Recovery中间件对于如果捕获异常,打印调用堆栈的信息都有一定的参考价值。