Golang : gin tutorial - 5 (Create a global error handler)

·

4 min read

Golang : gin tutorial - 5 (Create a global error handler)

Now that we know how to write few basic endpoints such as GET, POST (JSON consumption), POST (multipart/form-data). We have seen that there are situations where we return an error and we need to return the error in the API response body so that the API consumer knows what went wrong as their request was not full-filled.

We can write a gin middleware and use it as a global error handler. But before we write a middleware we need to write a custom error implementation.

Step 1

Create a file errors.go in a directory named error in the project.

image.png

This is what a custom error implementation would look like.

package error

import "fmt"

type Http struct {
    Description string `json:"description,omitempty"`
    Metadata    string `json:"metadata,omitempty"`
    StatusCode  int    `json:"statusCode"`
}

func (e Http) Error() string {
    return fmt.Sprintf("description: %s,  metadata: %s", e.Description, e.Metadata)
}

func NewHttpError(description, metadata string, statusCode int) Http {
    return Http{
        Description: description,
        Metadata:    metadata,
        StatusCode:  statusCode,
    }
}

Now, that we have a custom error implementation we need to write a gin middleware to use as a global error handler.

Step 2

Create a file error.go in a directory named middleware in the project.

image.png

This is what a global error handler would look like.

package middleware

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "practice/error"
)

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        for _, err := range c.Errors {
            switch e := err.Err.(type) {
            case error.Http:
                c.AbortWithStatusJSON(e.StatusCode, e)
            default:
                c.AbortWithStatusJSON(http.StatusInternalServerError, map[string]string{"message": "Service Unavailable"})
            }
        }
    }
}

Step 3

Now that we have a global error handler in place we need to register it so that gin can make use of it. Register the above created ErrorHandler in the main.go file.

func main(){
    //.......................................
    engine.Use(
        middleware.ErrorHandler(),
    )

    v1 := engine.Group("/api/v1")
    {
        v1.GET("/contents", content.GetContents)
        v1.POST("/contents", content.PostContents)
        v1.POST("/contents/import", content.ConsumeFile)
    }
    //.......................................

Step 4

Just by creating an error handler and registering it would not work. We will have to add the above created Http struct object in to the gin context instead of the generic errors which we have been adding to the gin context.

In controller.go file in the content directory we currently have something like this when we encounter an error in an endpoint.

ctx.Error(fmt.Errorf("name is required"))
ctx.AbortWithStatus(http.StatusBadRequest)
return

Now, this has to changed to something like this in the GET endpoint

func GetContents(ctx *gin.Context) {
    name, present := ctx.GetQuery("name")
    if !present {
        err := httperror.NewHttpError("Query parameter not found", "name query parameter is required", http.StatusBadRequest)
        ctx.Error(err)
        return
    }
    response := make(map[string]string, 0)
    response["name"] = name

    ctx.JSON(http.StatusOK, response)
}

And the POST endpoint would look like this

func PostContents(ctx *gin.Context) {
    var content content
    if err := ctx.ShouldBindJSON(&content); err != nil {
        err := httperror.NewHttpError("Invalid request body", "", http.StatusBadRequest)
        ctx.Error(err)
        return
    }
    ctx.JSON(http.StatusOK, content)
}

Something similar can be done in the multipart/form-data POST endpoint to look like this

func ConsumeFile(ctx *gin.Context) {
    fileHeader, err := ctx.FormFile("file")
    if err != nil {
        err := httperror.NewHttpError("file not found", "", http.StatusBadRequest)
        ctx.Error(err)
        return
    }

    //Open received file
    csvFileToImport, err := fileHeader.Open()
    if err != nil {
        err := httperror.NewHttpError("Invalid file", "", http.StatusBadRequest)
        ctx.Error(err)
        return
    }
    defer csvFileToImport.Close()

    //Create temp file
    tempFile, err := ioutil.TempFile("", fileHeader.Filename)
    if err != nil {
        err := httperror.NewHttpError("Error while creating temp file", "", http.StatusBadRequest)
        ctx.Error(err)
        return
    }
    defer tempFile.Close()

    //Delete temp file after importing
    defer os.Remove(tempFile.Name())

    //Write data from received file to temp file
    fileBytes, err := ioutil.ReadAll(csvFileToImport)
    if err != nil {
        err := httperror.NewHttpError("Error while wrting data to temp file", "", http.StatusBadRequest)
        ctx.Error(err)
        return
    }
    _, err = tempFile.Write(fileBytes)
    if err != nil {
        err := httperror.NewHttpError("Error while writing data to temp file", "", http.StatusBadRequest)
        ctx.Error(err)
        return
    }

    ctx.JSON(http.StatusOK, string(fileBytes))
}

Step 5

Now that we have created the middleware and registered it so that the gin server can use and we have also made changes in the controller to adapt to use the new errors. We can run our unit tests. If we run them we can see that some tests fail are failing. It is because we have not registered the middleware to the gin engine in tests. So, after creating the TextContext of gin, we need to register the error handler middleware to it for usage.

ctx, engine := gin.CreateTestContext(responseRecorder)
//Add the below line after we create the text context as shown in the above line
engine.Use(middleware.ErrorHandler())

Now, run the unit tests and we can see that the tests seem to work fine.

Step 6

Now, when we run the application using the main.go file and access the GET endpoint without the query parameter we can see the below error with a 400 status.

{
    "description": "Query parameter not found",
    "metadata": "name query parameter is required",
    "statusCode": 400
}

A similar error structure would occur when we try to access any endpoint we created without the required query parameters or the request body etc.

Did you find this article valuable?

Support Deepak by becoming a sponsor. Any amount is appreciated!