How to handle errors in Gin middleware
Asked Answered
O

2

17

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 ?

Ozonide answered 12/11, 2021 at 20:38 Comment(0)
H
55

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].

Hire answered 12/11, 2021 at 20:52 Comment(3)
Will the error being returned in the response be removed in the c.Errors automatically?Louie
@Louie c is request scoped, so at the end of the middleware chain that context ceases to exist and errors are cleared.Hire
If you are asking whether handler errors are stored in c.Errors automatically, then no, you have to call some function on c that takes also error arguments to propagate errorsHire
A
0

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 (
    "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()
}
  • Or map error to one response
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()
}
Acetanilide answered 23/2, 2023 at 2:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.