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 ?
CodePudding user response:
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)
}
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{"error": ErrNotFound.Error()})
}
// etc...
}
c.JSON(http.StatusInternalServerError, "")
}
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) > 0
and access only the first item c.Errors[0]
.