1. // recovery 机制,将协程中的函数异常进行捕获
    2. func Recovery() framework.ControllerHandler {
    3. // 使用函数回调
    4. return func(c *framework.Context) error {
    5. // 核心在增加这个 recover 机制,捕获 c.Next()出现的 panic
    6. defer func() {
    7. if err := recover(); err != nil {
    8. c.Json(500, err)
    9. }
    10. }()
    11. // 使用 next 执行具体的业务逻辑
    12. c.Next()
    13. return nil
    14. }
    15. }

    这个中间件的作用是捕获协程中的函数异常。我们使用 defer、recover 函数,捕获了 c.Next 中抛出的异常,并且在 HTTP 请求中返回 500 内部错误的状态码。
    乍看这段代码,是没有什么问题的。但是我们再仔细思考下是否有需要完善的细节?

    首先是异常类型,我们原先认为,所有异常都可以通过状态码,直接将异常状态返回给调用方,但是这里是有问题的。这里的异常,除了业务逻辑的异常,是不是也有可能是底层连接的异常?

    以底层连接中断异常为例,对于这种连接中断,我们是没有办法通过设置 HTTP 状态码来让浏览器感知的,并且一旦中断,后续的所有业务逻辑都没有什么作用了。同时,如果我们持续给已经中断的连接发送请求,会在底层持续显示网络连接错误(broken pipe)。

    1. return func(c *Context) {
    2. defer func() {
    3. if err := recover(); err != nil {
    4. // 判断是否是底层连接异常,如果是的话,则标记 brokenPipe
    5. var brokenPipe bool
    6. if ne, ok := err.(*net.OpError); ok {
    7. if se, ok := ne.Err.(*os.SyscallError); ok {
    8. if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
    9. brokenPipe = true
    10. }
    11. }
    12. }
    13. ...
    14. if brokenPipe {
    15. // 如果有标记位,我们不能写任何的状态码
    16. c.Error(err.(error)) // nolint: errcheck
    17. c.Abort()
    18. } else {
    19. handle(c, err)
    20. }
    21. }
    22. }()
    23. c.Next()
    24. }

    这段代码先判断了底层抛出的异常是否是网络异常(net.OpError),如果是的话,再根据异常内容是否包含“broken pipe”或者“connection reset by peer”,来判断这个异常是否是连接中断的异常。如果是,就设置标记位,并且直接使用 c.Abort() 来中断后续的处理逻辑。