如何处理 Gin 中间件中的错误

huangapple go评论90阅读模式
英文:

How to handle errors in Gin middleware

问题

我想在每个路由上捕获所有的HTTP错误,而不需要每次都重写400、404、500等错误处理逻辑。所以我在每个路由处理程序中有一个ErrorHandler()函数:

func (h *Handler) List(c *gin.Context) {
    movies, err := h.service.ListService()

    if err != nil {
        utils.ErrorHandler(c, err)
        return
    }

    c.JSON(http.StatusOK, movies)
}

这个函数看起来像这样:

func ErrorHandler(c *gin.Context, err error) {
    if err == ErrNotFound {
        // 404
        c.JSON(http.StatusNotFound, gin.H{"error": ErrNotFound.Error()})
    } else if err == ErrInternalServerError {
        // 500
        c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError.Error()})
    } // 等等...
}

ErrNotFoundErrInternalServerError只是像这样初始化的全局变量:

var ErrNotFound = errors.New(http.StatusText(http.StatusNotFound))  // 404

我想知道我是否做得对,或者是否有更好的方法,比如在中间件中获取错误并直接返回响应?

在Node.js中,我可以将err发送到中间件参数中,并像这样使用它:

app.use((err, req, res, next) => {
    if (err instanceof HttpError) {
        res.status(err.status).json({error: err.message});
    } else if (err instanceof Error) {
        res.status(500).json({error: err.message});
    } else {
        res.status(500).send("Internal Server Error");
    }
});

有类似的方法吗?

英文:

I want to grab all http errors on each route without rewrite each time if 400 then if 404 then if 500 then etc... so I have an ErrorHandler() function inside each route handler:

func (h *Handler) List(c *gin.Context) {
	movies, err := h.service.ListService()

	if err != nil {
		utils.ErrorHandler(c, err)
		return
	}

	c.JSON(http.StatusOK, movies)
}

This function look like this:

func ErrorHandler(c *gin.Context, err error) {
	if err == ErrNotFound {
        // 404
		c.JSON(http.StatusNotFound, gin.H{"error": ErrNotFound.Error()})
	} else if err == ErrInternalServerError {
        // 500
		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError.Error()})
	} // etc...
}

ErrNotFound or ErrInternalServerError are just global variables initialized like this :

var ErrNotFound = errors.New(http.StatusText(http.StatusNotFound))  // 404

I'd like to know if I'm doing right or if there is a better way to do this like grab the error inside the middleware and return directly the response ?

With node.js I was able to send err in the middleware parameter and use it like this:

app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
    if (err instanceof HttpError) {
        res.status(err.status).json({error: err.message});
      } else if (err instanceof Error) {
        res.status(500).json({error: err.message});
      } else {
        res.status(500).send("Internal Server Error");
      }
});

There is something similar ?

答案1

得分: 39

比起使用一个函数(utils 也不被推荐作为包名),使用一个中间件更加习惯用法:

func ErrorHandler(c *gin.Context) {
    c.Next()

    for _, err := range c.Errors {
        // 记录日志、处理错误等等
    }

    c.JSON(http.StatusInternalServerError, "")
}

func main() {
    router := gin.New()
    router.Use(middleware.ErrorHandler)
    // ... 路由
}

需要注意的是,在中间件函数中,在实际的错误处理代码之前调用 c.Next(),这样可以确保错误处理发生在处理链的其余部分之后。

Next 只应在中间件内部使用。它在调用处理程序内部执行待处理的处理程序。[...]

使用中间件的优势在于你可以传递参数给它,例如一个日志记录器,你可能希望在错误处理的一部分中使用它,只需传递一次,而不是每次直接调用 utils.ErrorHandler 时都传递一次。在这种情况下,代码如下(我使用了 Uber Zap 日志记录器):

func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        for _, ginErr := range c.Errors {
            logger.Error("糟糕", ...)
        }
    }
}

func main() {
    router := gin.New()

    logger, _ := zap.NewDevelopment()

    router.Use(middleware.ErrorHandler(logger))
    // ... 路由
}

然后处理程序只会中止处理链,而不是调用一个函数,这看起来更加简洁,更容易维护:

func (h *Handler) List(c *gin.Context) {
    movies, err := h.service.ListService()

    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }

    c.JSON(http.StatusOK, movies)
}

需要注意的是,如果你在 c.AbortWithStatusc.AbortWithError 中设置了 HTTP 状态码,你可能不希望在错误处理程序中覆盖它。在这种情况下,你可以使用 -1 作为状态码调用 c.JSON()

func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        for _, ginErr := range c.Errors {
            logger.Error("糟糕", ...)
        }

        // 状态码 -1 不会覆盖现有的状态码
        c.JSON(-1, /* 错误负载 */)
    }
}

最后,使用中间件允许你在处理程序中多次调用 c.Error,例如当一系列非致命错误发生时,你希望在实际中止请求之前捕获所有这些错误。

Error 将错误附加到当前上下文中。错误被推送到错误列表中。在请求解析过程中发生的每个错误都调用 Error 是一个好主意。可以使用中间件来收集所有的错误并[处理它们]。

func (h *Handler) List(c *gin.Context) {
    err1 := /* 非致命错误 */
    if err1 != nil {
        c.Error(err1)
    }

    err2 := /* 另一个非致命错误 */
    if err2 != nil {
        c.Error(err2)
    }

    fatalErr := /* 致命错误 */
    if fatalErr != nil {
        c.AbortWithError(505, fatalErr)
        return
        // 错误处理程序将收集到所有 3 个错误
    }

    c.JSON(http.StatusOK, movies)
}

至于中间件中的实际错误处理,它非常简单。只需记住,所有对 c.Errorc.AbortWith... 的调用都会将你的错误包装在 gin.Error 中。因此,要检查原始值,你必须检查 err.Err 字段:

func ErrorHandler(c *gin.Context) {
    c.Next()

    for _, err := range c.Errors {
        switch err.Err {
        case ErrNotFound:
            c.JSON(-1, gin.H{"error": ErrNotFound.Error()})
        }
        // 等等...
    }

    c.JSON(http.StatusInternalServerError, "")
}

遍历 c.Errors 可能看起来有些麻烦,因为现在你可能有 N 个错误而不是一个,但根据你打算如何使用中间件,你可以简单地检查 len(c.Errors) > 0 并且只访问第一个项 c.Errors[0]

英文:

More idiomatic than using a function (utils is also frowned upon as a package name) is using a middleware:

func ErrorHandler(c *gin.Context) {
		c.Next()

		for _, err := range c.Errors {
			// log, handle, etc.
		}
	
		c.JSON(http.StatusInternalServerError, "")
}


func main() {
	router := gin.New()
	router.Use(middleware.ErrorHandler)
    // ... routes
}

Notably, you call c.Next() inside the middleware func before your actual error handling code, so you make sure the error handling happens after the rest of the handler chain has been called.

> Next should be used only inside middleware. It executes the pending handlers in the chain inside the calling handler. [...]

The advantage of using a middleware is that you can also pass arguments to it, e.g. a logger, that you may want to use later as part of the error handling, once, instead of passing it every time you call utils.ErrorHandler directly. In this case it looks like this (I use Uber Zap loggers):

func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()
		
		for _, ginErr := range c.Errors {
			logger.Error("whoops", ...)
		}
	}
}

func main() {
	router := gin.New()
    
    logger, _ := zap.NewDevelopment()

	router.Use(middleware.ErrorHandler(logger))
    // ... routes
}

The handlers then will just abort the chain, instead of calling a function, which looks cleaner and it's easier to maintain:

func (h *Handler) List(c *gin.Context) {
    movies, err := h.service.ListService()

    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }

    c.JSON(http.StatusOK, movies)
}

It's important to note that if you set an HTTP status in c.AbortWithStatus or c.AbortWithError, you may want to not overwrite it in the error handler. In that case, you can call c.JSON() with -1 as status code:

func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()
		
		for _, ginErr := range c.Errors {
			logger.Error("whoops", ...)
		}

        // status -1 doesn't overwrite existing status code
        c.JSON(-1, /* error payload */)
	}
}

Lastly, using a middleware allows you to call c.Error in your handlers multiple times, e.g. when a series of non-fatal errors occur and you want to capture all of them before actually aborting the request.

> Error attaches an error to the current context. The error is pushed to a list of errors. It's a good idea to call Error for each error that occurred during the resolution of a request. A middleware can be used to collect all the errors and [process them]

func (h *Handler) List(c *gin.Context) {
    err1 := /* non-fatal error */
    if err1 != nil {
        c.Error(err1)
    }

    err2 := /* another non-fatal error */
    if err2 != nil {
        c.Error(err2)
    }

    fatalErr := /* fatal error */
    if fatalErr != nil {
        c.AbortWithError(505, fatalErr)
        return
        // the error handler will have collected all 3 errors
    }

    c.JSON(http.StatusOK, movies)
}

<hr>

As for the actual error handling in the middleware, it's pretty straightforward. Just remember that all calls to c.Error, c.AbortWith... will wrap your error in a gin.Error. So to inspect the original value you have to check the err.Err field:

func ErrorHandler(c *gin.Context) {
        c.Next()

        for _, err := range c.Errors {
            switch err.Err {
                case ErrNotFound:
                  c.JSON(-1, gin.H{&quot;error&quot;: ErrNotFound.Error()})  
            }
            // etc...
        }

        c.JSON(http.StatusInternalServerError, &quot;&quot;)
}

Iterating over c.Errors may seem unwieldy because now you have potentially N errors instead of one, but depending on how you intend to use the middleware, you can simply check len(c.Errors) &gt; 0 and access only the first item c.Errors[0].

答案2

得分: 1

在中间件中处理错误的好处是可以将错误处理集中化,而不是在每个处理程序中显式处理异常。

为了减少将错误映射到响应中的次数,我的项目 gin-error 是一个用于错误处理的中间件。

func Error(errM ...*ErrorMap) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		lastError := c.Errors.Last()
		if lastError == nil {
			return
		}

		for _, err := range errM {
			for _, e := range err.errors {
				if e == lastError.Err || errors.Is(e, lastError.Err) {
					err.response(c)
				}
			}
		}
	}
}

使用示例:

  • 您可以将错误映射到一个状态码
import (
    "github.com/richzw/gin-error"
    "github.com/gin-gonic/gin"
)
var BadRequestErr = fmt.Errorf("bad request error")

func main() {
    r := gin.Default()
    r.Use(err.Error(err.NewErrMap(BadRequestErr).StatusCode(http.StatusBadRequest)))

    r.GET("/test", func(c *gin.Context) {
        c.Error(BadRequestErr)
    })

    r.Run()
}
  • 或将错误映射到一个响应
import (
    "github.com/richzw/gin-error"
    "github.com/gin-gonic/gin"
)
var BadRequestErr = fmt.Errorf("bad request error")

func main() {
    r := gin.Default()
    r.Use(err.Error(
        err.NewErrMap(BadRequestErr).Response(func(c *gin.Context) {
            c.JSON(http.StatusBadRequest, gin.H{"error": BadRequestErr.Error()})
        })))

    r.GET("/test", func(c *gin.Context) {
        c.Error(BadRequestErr)
    })

    r.Run()
}
英文:

The benefit of handling errors in middleware is handling errors centralized, as opposed to explicitly handling exceptions in each handler.

In order to reduce mapping errors to response, my project
gin-error
is one middleware for error handling.

func Error(errM ...*ErrorMap) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		lastError := c.Errors.Last()
		if lastError == nil {
			return
		}

		for _, err := range errM {
			for _, e := range err.errors {
				if e == lastError.Err || errors.Is(e, lastError.Err) {
					err.response(c)
				}
			}
		}
	}
}

Usage Sample:

  • You can map the error to one status code
import (
    &quot;github.com/richzw/gin-error&quot;
    &quot;github.com/gin-gonic/gin&quot;
)
var BadRequestErr = fmt.Errorf(&quot;bad request error&quot;)

func main() {
    r := gin.Default()
    r.Use(err.Error(err.NewErrMap(BadRequestErr).StatusCode(http.StatusBadRequest)))

    r.GET(&quot;/test&quot;, func(c *gin.Context) {
        c.Error(BadRequestErr)
    })

    r.Run()
}
  • Or map error to one response
import (
    &quot;github.com/richzw/gin-error&quot;
    &quot;github.com/gin-gonic/gin&quot;
)
var BadRequestErr = fmt.Errorf(&quot;bad request error&quot;)

func main() {
    r := gin.Default()
    r.Use(err.Error(
        err.NewErrMap(BadRequestErr).Response(func(c *gin.Context) {
            c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: BadRequestErr.Error()})
        })))

    r.GET(&quot;/test&quot;, func(c *gin.Context) {
        c.Error(BadRequestErr)
    })

    r.Run()
}

huangapple
  • 本文由 发表于 2021年11月13日 04:38:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/69948784.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定