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中间件对于如果捕获异常,打印调用堆栈的信息都有一定的参考价值。