mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-26 22:52:16 +00:00
feat: Add centralized error handling with typed error codes, validation support, and enhanced parsing utilities
- Add error code constants for validation/auth/resource/system/business logic errors (VALIDATION_FAILED/UNAUTHORIZED/NOT_FOUND/INTERNAL_ERROR/etc) - Add EnhancedErrorResponse with error/code/details/context/timestamp/request_id fields and ValidationErrorResponse with field-specific errors array - Add handleError function with gorm.ErrRecordNotFound/ErrDuplicatedKey/ErrInvalidData handling
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Error codes for different types of errors
|
||||
const (
|
||||
// Validation errors
|
||||
ErrCodeValidationFailed = "VALIDATION_FAILED"
|
||||
ErrCodeInvalidInput = "INVALID_INPUT"
|
||||
ErrCodeMissingRequired = "MISSING_REQUIRED"
|
||||
ErrCodeInvalidFormat = "INVALID_FORMAT"
|
||||
|
||||
// Authentication/Authorization errors
|
||||
ErrCodeUnauthorized = "UNAUTHORIZED"
|
||||
ErrCodeForbidden = "FORBIDDEN"
|
||||
ErrCodeInvalidToken = "INVALID_TOKEN"
|
||||
ErrCodeTokenExpired = "TOKEN_EXPIRED"
|
||||
ErrCodeInsufficientPerms = "INSUFFICIENT_PERMISSIONS"
|
||||
|
||||
// Resource errors
|
||||
ErrCodeNotFound = "NOT_FOUND"
|
||||
ErrCodeAlreadyExists = "ALREADY_EXISTS"
|
||||
ErrCodeConflict = "CONFLICT"
|
||||
ErrCodeResourceLocked = "RESOURCE_LOCKED"
|
||||
|
||||
// System errors
|
||||
ErrCodeInternalError = "INTERNAL_ERROR"
|
||||
ErrCodeDatabaseError = "DATABASE_ERROR"
|
||||
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
|
||||
ErrCodeTimeout = "TIMEOUT"
|
||||
ErrCodeRateLimited = "RATE_LIMITED"
|
||||
|
||||
// Business logic errors
|
||||
ErrCodeInvalidState = "INVALID_STATE"
|
||||
ErrCodeQuotaExceeded = "QUOTA_EXCEEDED"
|
||||
ErrCodeOperationFailed = "OPERATION_FAILED"
|
||||
)
|
||||
|
||||
// EnhancedErrorResponse represents an enhanced error response with additional context
|
||||
type EnhancedErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Context any `json:"context,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationError represents field-specific validation errors
|
||||
type ValidationError struct {
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationErrorResponse represents a response with multiple validation errors
|
||||
type ValidationErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code"`
|
||||
Errors []ValidationError `json:"errors"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
// handleError is the central error handling function
|
||||
func handleError(c *gin.Context, err error, defaultMessage string, defaultCode string) {
|
||||
// Analyze the error type and provide appropriate response
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
c.JSON(http.StatusNotFound, ErrorResponse{
|
||||
Error: "Resource not found",
|
||||
Code: ErrCodeNotFound,
|
||||
Details: fmt.Sprintf("The requested resource could not be found: %s", err.Error()),
|
||||
})
|
||||
|
||||
case errors.Is(err, gorm.ErrDuplicatedKey):
|
||||
c.JSON(http.StatusConflict, ErrorResponse{
|
||||
Error: "Resource already exists",
|
||||
Code: ErrCodeAlreadyExists,
|
||||
Details: "A resource with this identifier already exists",
|
||||
})
|
||||
|
||||
case errors.Is(err, gorm.ErrInvalidTransaction):
|
||||
fallthrough
|
||||
case errors.Is(err, gorm.ErrInvalidData):
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "Invalid data",
|
||||
Code: ErrCodeInvalidInput,
|
||||
Details: fmt.Sprintf("The provided data is invalid: %s", err.Error()),
|
||||
})
|
||||
|
||||
case isValidationError(err):
|
||||
handleValidationError(c, err)
|
||||
|
||||
case isAuthError(err):
|
||||
c.JSON(http.StatusUnauthorized, ErrorResponse{
|
||||
Error: "Authentication failed",
|
||||
Code: ErrCodeUnauthorized,
|
||||
Details: err.Error(),
|
||||
})
|
||||
|
||||
case isPermissionError(err):
|
||||
c.JSON(http.StatusForbidden, ErrorResponse{
|
||||
Error: "Access denied",
|
||||
Code: ErrCodeForbidden,
|
||||
Details: "You don't have permission to perform this action",
|
||||
})
|
||||
|
||||
case isTimeoutError(err):
|
||||
c.JSON(http.StatusRequestTimeout, ErrorResponse{
|
||||
Error: "Request timeout",
|
||||
Code: ErrCodeTimeout,
|
||||
Details: "The request took too long to process",
|
||||
})
|
||||
|
||||
case isRateLimitError(err):
|
||||
c.JSON(http.StatusTooManyRequests, ErrorResponse{
|
||||
Error: "Rate limit exceeded",
|
||||
Code: ErrCodeRateLimited,
|
||||
Details: "Too many requests, please try again later",
|
||||
})
|
||||
|
||||
default:
|
||||
// Log the error for debugging
|
||||
c.Error(err)
|
||||
|
||||
statusCode := http.StatusInternalServerError
|
||||
if strings.Contains(strings.ToLower(err.Error()), "not found") {
|
||||
statusCode = http.StatusNotFound
|
||||
} else if strings.Contains(strings.ToLower(err.Error()), "unauthorized") {
|
||||
statusCode = http.StatusUnauthorized
|
||||
} else if strings.Contains(strings.ToLower(err.Error()), "forbidden") {
|
||||
statusCode = http.StatusForbidden
|
||||
} else if strings.Contains(strings.ToLower(err.Error()), "invalid") {
|
||||
statusCode = http.StatusBadRequest
|
||||
}
|
||||
|
||||
c.JSON(statusCode, ErrorResponse{
|
||||
Error: defaultMessage,
|
||||
Code: defaultCode,
|
||||
Details: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handleValidationError handles validation errors with field-specific details
|
||||
func handleValidationError(c *gin.Context, err error) {
|
||||
var validationErrors []ValidationError
|
||||
|
||||
// Handle validator.ValidationErrors
|
||||
if validationErrs, ok := err.(validator.ValidationErrors); ok {
|
||||
for _, fieldErr := range validationErrs {
|
||||
validationErrors = append(validationErrors, ValidationError{
|
||||
Field: fieldErr.Field(),
|
||||
Message: getValidationErrorMessage(fieldErr),
|
||||
Value: fmt.Sprintf("%v", fieldErr.Value()),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Handle other validation errors
|
||||
validationErrors = append(validationErrors, ValidationError{
|
||||
Field: "general",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, ValidationErrorResponse{
|
||||
Error: "Validation failed",
|
||||
Code: ErrCodeValidationFailed,
|
||||
Errors: validationErrors,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions for error type detection
|
||||
func isValidationError(err error) bool {
|
||||
_, ok := err.(validator.ValidationErrors)
|
||||
return ok || strings.Contains(strings.ToLower(err.Error()), "validation")
|
||||
}
|
||||
|
||||
func isAuthError(err error) bool {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
return strings.Contains(errStr, "unauthorized") ||
|
||||
strings.Contains(errStr, "authentication") ||
|
||||
strings.Contains(errStr, "invalid credentials") ||
|
||||
strings.Contains(errStr, "invalid token")
|
||||
}
|
||||
|
||||
func isPermissionError(err error) bool {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
return strings.Contains(errStr, "forbidden") ||
|
||||
strings.Contains(errStr, "permission") ||
|
||||
strings.Contains(errStr, "access denied") ||
|
||||
strings.Contains(errStr, "insufficient")
|
||||
}
|
||||
|
||||
func isTimeoutError(err error) bool {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
return strings.Contains(errStr, "timeout") ||
|
||||
strings.Contains(errStr, "deadline exceeded")
|
||||
}
|
||||
|
||||
func isRateLimitError(err error) bool {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
return strings.Contains(errStr, "rate limit") ||
|
||||
strings.Contains(errStr, "too many requests")
|
||||
}
|
||||
|
||||
// getValidationErrorMessage converts validator.FieldError to user-friendly message
|
||||
func getValidationErrorMessage(fieldErr validator.FieldError) string {
|
||||
switch fieldErr.Tag() {
|
||||
case "required":
|
||||
return fmt.Sprintf("%s is required", fieldErr.Field())
|
||||
case "email":
|
||||
return fmt.Sprintf("%s must be a valid email address", fieldErr.Field())
|
||||
case "min":
|
||||
return fmt.Sprintf("%s must be at least %s characters long", fieldErr.Field(), fieldErr.Param())
|
||||
case "max":
|
||||
return fmt.Sprintf("%s must be at most %s characters long", fieldErr.Field(), fieldErr.Param())
|
||||
case "len":
|
||||
return fmt.Sprintf("%s must be exactly %s characters long", fieldErr.Field(), fieldErr.Param())
|
||||
case "numeric":
|
||||
return fmt.Sprintf("%s must be a number", fieldErr.Field())
|
||||
case "alpha":
|
||||
return fmt.Sprintf("%s must contain only letters", fieldErr.Field())
|
||||
case "alphanum":
|
||||
return fmt.Sprintf("%s must contain only letters and numbers", fieldErr.Field())
|
||||
case "oneof":
|
||||
return fmt.Sprintf("%s must be one of: %s", fieldErr.Field(), fieldErr.Param())
|
||||
case "uuid":
|
||||
return fmt.Sprintf("%s must be a valid UUID", fieldErr.Field())
|
||||
case "url":
|
||||
return fmt.Sprintf("%s must be a valid URL", fieldErr.Field())
|
||||
case "datetime":
|
||||
return fmt.Sprintf("%s must be a valid datetime", fieldErr.Field())
|
||||
default:
|
||||
return fmt.Sprintf("%s is invalid", fieldErr.Field())
|
||||
}
|
||||
}
|
||||
|
||||
// getCurrentTimestamp returns the current timestamp in ISO 8601 format
|
||||
func getCurrentTimestamp() string {
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Convenience functions for common error scenarios
|
||||
func BadRequest(c *gin.Context, err error, details ...string) {
|
||||
message := "Invalid request"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeInvalidInput)
|
||||
}
|
||||
|
||||
func Unauthorized(c *gin.Context, err error, details ...string) {
|
||||
message := "Unauthorized"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeUnauthorized)
|
||||
}
|
||||
|
||||
func Forbidden(c *gin.Context, err error, details ...string) {
|
||||
message := "Access denied"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeForbidden)
|
||||
}
|
||||
|
||||
func NotFound(c *gin.Context, err error, details ...string) {
|
||||
message := "Resource not found"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeNotFound)
|
||||
}
|
||||
|
||||
func Conflict(c *gin.Context, err error, details ...string) {
|
||||
message := "Resource conflict"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeConflict)
|
||||
}
|
||||
|
||||
func InternalServerError(c *gin.Context, err error, details ...string) {
|
||||
message := "Internal server error"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeInternalError)
|
||||
}
|
||||
|
||||
func ServiceUnavailable(c *gin.Context, err error, details ...string) {
|
||||
message := "Service unavailable"
|
||||
if len(details) > 0 {
|
||||
message = details[0]
|
||||
}
|
||||
handleError(c, err, message, ErrCodeServiceUnavailable)
|
||||
}
|
||||
|
||||
// Enhanced parsing functions with better error handling
|
||||
func ParseUintParam(c *gin.Context, name string) (uint, bool) {
|
||||
value := c.Param(name)
|
||||
if value == "" {
|
||||
BadRequest(c, fmt.Errorf("parameter %s is required", name), fmt.Sprintf("Missing required parameter: %s", name))
|
||||
return 0, false
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseUint(value, 10, 32)
|
||||
if err != nil {
|
||||
BadRequest(c, err, fmt.Sprintf("Invalid %s parameter: must be a positive integer", name))
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return uint(parsed), true
|
||||
}
|
||||
|
||||
func ParseIntParam(c *gin.Context, name string) (int, bool) {
|
||||
value := c.Param(name)
|
||||
if value == "" {
|
||||
BadRequest(c, fmt.Errorf("parameter %s is required", name), fmt.Sprintf("Missing required parameter: %s", name))
|
||||
return 0, false
|
||||
}
|
||||
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
BadRequest(c, err, fmt.Sprintf("Invalid %s parameter: must be an integer", name))
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
func ParseBoolQuery(c *gin.Context, key string) *bool {
|
||||
raw := c.Query(key)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
BadRequest(c, err, fmt.Sprintf("Invalid %s query parameter: must be true or false", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &parsed
|
||||
}
|
||||
|
||||
func ParseIntQuery(c *gin.Context, key string, defaultValue int) (int, bool) {
|
||||
raw := c.Query(key)
|
||||
if raw == "" {
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
BadRequest(c, err, fmt.Sprintf("Invalid %s query parameter: must be an integer", key))
|
||||
return defaultValue, false
|
||||
}
|
||||
|
||||
return parsed, true
|
||||
}
|
||||
Reference in New Issue
Block a user