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