14
Custom validation errors
If you try to create new account using too short password, you will get error Key: 'User.Password' Error:Field validation for 'Password' failed on the 'min' tag
. This is not really user friendly, so it should be changed for better user experience. Let's see how we can transform that to our own custom error messages. For that purpose we will create new Gin handler functions in internal/server/middleware.go
file:
func customErrors(ctx *gin.Context) {
ctx.Next()
if len(ctx.Errors) > 0 {
for _, err := range ctx.Errors {
// Check error type
switch err.Type {
case gin.ErrorTypePublic:
// Show public errors only if nothing has been written yet
if !ctx.Writer.Written() {
ctx.AbortWithStatusJSON(ctx.Writer.Status(), gin.H{"error": err.Error()})
}
case gin.ErrorTypeBind:
errMap := make(map[string]string)
if errs, ok := err.Err.(validator.ValidationErrors); ok {
for _, fieldErr := range []validator.FieldError(errs) {
errMap[fieldErr.Field()] = customValidationError(fieldErr)
}
}
status := http.StatusBadRequest
// Preserve current status
if ctx.Writer.Status() != http.StatusOK {
status = ctx.Writer.Status()
}
ctx.AbortWithStatusJSON(status, gin.H{"error": errMap})
default:
// Log other errors
log.Error().Err(err.Err).Msg("Other error")
}
}
// If there was no public or bind error, display default 500 message
if !ctx.Writer.Written() {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": InternalServerError})
}
}
}
func customValidationError(err validator.FieldError) string {
switch err.Tag() {
case "required":
return fmt.Sprintf("%s is required.", err.Field())
case "min":
return fmt.Sprintf("%s must be longer than or equal %s characters.", err.Field(), err.Param())
case "max":
return fmt.Sprintf("%s cannot be longer than %s characters.", err.Field(), err.Param())
default:
return err.Error()
}
}
Constant InternalServerError
is defined in internal/server/server.go
:
const InternalServerError = "Something went wrong!"
Let's use new Gin middleware in internal/server/router.go
:
func setRouter() *gin.Engine {
// Creates default gin router with Logger and Recovery middleware already attached
router := gin.Default()
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
router.RedirectTrailingSlash = true
// Create API route group
api := router.Group("/api")
api.Use(customErrors)
{
api.POST("/signup", gin.Bind(store.User{}), signUp)
api.POST("/signin", gin.Bind(store.User{}), signIn)
}
authorized := api.Group("/")
authorized.Use(authorization)
{
authorized.GET("/posts", indexPosts)
authorized.POST("/posts", createPost)
authorized.PUT("/posts", updatePost)
authorized.DELETE("/posts/:id", deletePost)
}
router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })
return router
}
As you can see, we are now using customErrors
middleware in api
group. But that's not the only change. Notice updated routes for signing in and signing up:
api.POST("/signup", gin.Bind(store.User{}), signUp)
api.POST("/signin", gin.Bind(store.User{}), signIn)
With these changes, we are trying to bind request data before even hitting signUp and signIn handlers, which means that handlers will only be reached if form validations are passed. With setup like this, handlers don't need to think about binding errors, because there was none if handler is reached. With that in mind, let's update these 2 handlers:
func signUp(ctx *gin.Context) {
user := ctx.MustGet(gin.BindKey).(*store.User)
if err := store.AddUser(user); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Signed up successfully.",
"jwt": generateJWT(user),
})
}
func signIn(ctx *gin.Context) {
user := ctx.MustGet(gin.BindKey).(*store.User)
user, err := store.Authenticate(user.Username, user.Password)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Sign in failed."})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Signed in successfully.",
"jwt": generateJWT(user),
})
}
Our handlers are much simpler now and they are only handling database errors. If you again try to create account with too short username and password, you will see more readable and descriptive errors:
We can now do same thing with Post handlers. I will not show you the solution here, so you can try it yourself for practice, but you can find it on RGB GitHub repo.
14