package handlers import ( "encoding/json" "errors" "io" "net/http" "strings" "github.com/gin-gonic/gin" ) func RespondError(c *gin.Context, status int, fallback string, err error) { if err != nil { _ = c.Error(err) } c.JSON(status, gin.H{"error": clientFacingErrorMessage(status, fallback, err)}) } // BindJSON decodes the request body into T, responding with a 422 and writing // the error via RespondError on failure. The second return value reports success. func BindJSON[T any](c *gin.Context) (*T, bool) { var req T if err := c.ShouldBindJSON(&req); err != nil { RespondError(c, http.StatusUnprocessableEntity, "invalid request", err) return nil, false } return &req, true } func clientFacingErrorMessage(status int, fallback string, err error) string { if err == nil { return fallback } if status >= 500 { return fallback } if isRequestDecodeError(err) { return fallback } message := strings.TrimSpace(err.Error()) if message == "" { return fallback } if looksTechnicalError(message) { return fallback } return message } func isRequestDecodeError(err error) bool { var syntaxErr *json.SyntaxError if errors.As(err, &syntaxErr) { return true } var unmarshalTypeErr *json.UnmarshalTypeError if errors.As(err, &unmarshalTypeErr) { return true } return errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) } func looksTechnicalError(message string) bool { lower := strings.ToLower(strings.TrimSpace(message)) needles := []string{ "sql", "gorm", "driver", "constraint", "syntax error", "unexpected eof", "record not found", "no such table", "stack trace", } for _, needle := range needles { if strings.Contains(lower, needle) { return true } } return false }