Files
2026-04-14 01:21:54 +00:00

207 lines
5.4 KiB
Go

package errors
import (
"encoding/json"
"fmt"
"net/http"
"service/pkg/logger"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// HTTPErrorResponse represents standardized HTTP error response
type HTTPErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
Retryable bool `json:"retryable,omitempty"`
} `json:"error"`
Meta struct {
HTTPStatus int `json:"http_status"`
Category string `json:"category"`
} `json:"meta,omitempty"`
}
// HTTPMiddleware creates error handling middleware for Gin
func HTTPMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// Check if there are any errors
if len(c.Errors) > 0 {
// Get the last error
err := c.Errors.Last().Err
HandleHTTPError(c, err)
}
}
}
// HandleHTTPError handles HTTP error response
func HandleHTTPError(c *gin.Context, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
}
// Create error response
response := &HTTPErrorResponse{}
response.Error.Code = appErr.Code()
response.Error.Message = appErr.GetLocalizedMessage(getLanguageFromContext(c))
response.Error.Details = appErr.Metadata()
response.Error.RequestID = c.GetString("request_id")
response.Error.Timestamp = appErr.Metadata()["timestamp"].(string)
response.Error.Retryable = IsRetryable(appErr.Code())
response.Meta.HTTPStatus = appErr.HTTPStatus()
response.Meta.Category = appErr.Category()
// Log error
LogHTTPError(c, appErr)
// Send response
c.JSON(appErr.HTTPStatus(), response)
}
// getLanguageFromContext extracts language from context
func getLanguageFromContext(c *gin.Context) string {
// Try Accept-Language header
lang := c.GetHeader("Accept-Language")
if lang != "" {
return parseAcceptLanguage(lang)
}
// Try query parameter
lang = c.Query("lang")
if lang != "" {
return lang
}
// Default to English
return "en"
}
// parseAcceptLanguage parses Accept-Language header
func parseAcceptLanguage(header string) string {
// Simple parsing - take first language
for idx := 0; idx < len(header); idx++ {
if header[idx] == ',' || header[idx] == ';' {
return header[:idx]
}
}
return header
}
// LogHTTPError logs HTTP error with context
func LogHTTPError(c *gin.Context, err Error) {
log := logger.Default()
log.Error("HTTP error occurred",
logger.String("error_code", err.Code()),
logger.String("category", err.Category()),
logger.Int("http_status", err.HTTPStatus()),
logger.Any("metadata", err.Metadata()),
logger.String("path", c.Request.URL.Path),
logger.String("method", c.Request.Method),
)
}
// ValidationErrorHandler handles validation errors specifically
func ValidationErrorHandler(c *gin.Context, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeValidationFailed, err.Error())
}
// Enhance validation errors with field details
if appErr.Category() == CategoryValidation {
if details, ok := appErr.Metadata()["validation_errors"]; ok {
response := gin.H{
"error": gin.H{
"code": appErr.Code(),
"message": appErr.GetLocalizedMessage(getLanguageFromContext(c)),
"fields": details,
},
}
c.JSON(http.StatusBadRequest, response)
return
}
}
HandleHTTPError(c, appErr)
}
// PanicHandler handles panics
func PanicHandler(c *gin.Context, recovered interface{}) {
var err error
switch x := recovered.(type) {
case string:
err = NewWithCode(ErrCodeInternalError, x)
case error:
err = FromError(x)
default:
err = NewWithCode(ErrCodeUnexpectedError, fmt.Sprintf("Unknown panic: %v", x))
}
HandleHTTPError(c, err)
}
// WriteErrorResponse writes error response directly
func WriteErrorResponse(w http.ResponseWriter, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
}
response := &HTTPErrorResponse{}
response.Error.Code = appErr.Code()
response.Error.Message = appErr.GetLocalizedMessage("en")
response.Error.Details = appErr.Metadata()
response.Error.Timestamp = appErr.Metadata()["timestamp"].(string)
response.Error.Retryable = IsRetryable(appErr.Code())
response.Meta.HTTPStatus = appErr.HTTPStatus()
response.Meta.Category = appErr.Category()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(appErr.HTTPStatus())
json.NewEncoder(w).Encode(response)
}
// sanitizeError removes sensitive information from errors
func sanitizeError(err error) error {
appErr := FromError(err)
if appErr == nil {
return err
}
// Remove sensitive metadata
sanitized := make(Metadata)
for k, v := range appErr.Metadata() {
if !isSensitiveField(k) {
sanitized[k] = v
}
}
return NewWithMetadata(appErr.Code(), appErr.Error(), sanitized)
}
// isSensitiveField checks if field contains sensitive information
func isSensitiveField(field string) bool {
sensitiveFields := []string{"password", "token", "secret", "key", "auth"}
for _, sensitive := range sensitiveFields {
if strings.Contains(strings.ToLower(field), sensitive) {
return true
}
}
return false
}
// generateRequestID generates a unique request ID
func generateRequestID() string {
return fmt.Sprintf("req_%d", time.Now().UnixNano())
}