23
loading...
This website collects cookies to deliver better user experience
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()
}
}
InternalServerError
is defined in internal/server/server.go
:const InternalServerError = "Something went wrong!"
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
}
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)
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),
})
}