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

442 lines
11 KiB
Go

package errors
import (
"context"
"fmt"
"net/http"
"service/pkg/logger"
"time"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
// ErrorMiddlewareConfig configures error middleware
type ErrorMiddlewareConfig struct {
// SkipRoutes defines routes to skip error handling
SkipRoutes []string
// CustomHandlers defines custom error handlers
CustomHandlers map[string]ErrorHandlerFunc
// LogAllErrors enables logging all errors
LogAllErrors bool
// SanitizeErrors enables error sanitization
SanitizeErrors bool
// DefaultLanguage sets default language for error messages
DefaultLanguage string
// RequestIDHeader sets header name for request ID
RequestIDHeader string
// EnableMetrics enables error metrics collection
EnableMetrics bool
// Timeout sets timeout for error handling
Timeout time.Duration
// RecoveryEnabled enables panic recovery
RecoveryEnabled bool
// CircuitBreakerEnabled enables circuit breaker
CircuitBreakerEnabled bool
}
// ErrorHandlerFunc handles errors
type ErrorHandlerFunc func(*gin.Context, error)
// DefaultMiddlewareConfig returns default configuration
func DefaultMiddlewareConfig() *ErrorMiddlewareConfig {
return &ErrorMiddlewareConfig{
SkipRoutes: []string{"/health", "/metrics", "/ping"},
LogAllErrors: true,
SanitizeErrors: true,
DefaultLanguage: "en",
RequestIDHeader: "X-Request-ID",
EnableMetrics: true,
Timeout: 30 * time.Second,
RecoveryEnabled: true,
CircuitBreakerEnabled: false,
}
}
// ErrorMiddleware creates Gin middleware for error handling
func ErrorMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
var cfg *ErrorMiddlewareConfig
if len(config) > 0 {
cfg = config[0]
} else {
cfg = DefaultMiddlewareConfig()
}
return func(c *gin.Context) {
// Skip specified routes
for _, route := range cfg.SkipRoutes {
if c.Request.URL.Path == route {
c.Next()
return
}
}
// Generate request ID if not present
requestID := c.GetHeader(cfg.RequestIDHeader)
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
// Set language
lang := getLanguageFromContext(c)
if lang == "" {
lang = cfg.DefaultLanguage
}
c.Set("language", lang)
// Create context with timeout
if cfg.Timeout > 0 {
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
}
// Process request
c.Next()
// Handle errors
if len(c.Errors) > 0 {
handleErrors(c, c.Errors.Last().Err, cfg)
}
}
}
// RecoveryMiddleware creates recovery middleware
func RecoveryMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
var cfg *ErrorMiddlewareConfig
if len(config) > 0 {
cfg = config[0]
} else {
cfg = DefaultMiddlewareConfig()
}
return gin.CustomRecovery(func(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))
}
handleErrors(c, err, cfg)
})
}
// CombinedMiddleware creates combined error and recovery middleware
func CombinedMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
cfg := DefaultMiddlewareConfig()
if len(config) > 0 {
cfg = config[0]
}
return func(c *gin.Context) {
// Skip specified routes
for _, route := range cfg.SkipRoutes {
if c.Request.URL.Path == route {
c.Next()
return
}
}
// Generate request ID if not present
requestID := c.GetHeader(cfg.RequestIDHeader)
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
// Set language
lang := getLanguageFromContext(c)
if lang == "" {
lang = cfg.DefaultLanguage
}
c.Set("language", lang)
// Create context with timeout
if cfg.Timeout > 0 {
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
}
// Process request with recovery
defer func() {
if r := recover(); r != nil {
var err error
switch x := r.(type) {
case string:
err = NewWithCode(ErrCodeInternalError, x)
case error:
err = FromError(x)
default:
err = NewWithCode(ErrCodeUnexpectedError,
fmt.Sprintf("Unknown panic: %v", x))
}
handleErrors(c, err, cfg)
}
}()
c.Next()
// Handle errors
if len(c.Errors) > 0 {
handleErrors(c, c.Errors.Last().Err, cfg)
}
}
}
// handleErrors handles errors with configuration
func handleErrors(c *gin.Context, err error, cfg *ErrorMiddlewareConfig) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
}
// Apply custom handlers
if handler, exists := cfg.CustomHandlers[appErr.Code()]; exists {
handler(c, appErr)
return
}
// Apply registered handlers
appErr = ApplyHandlers(appErr)
// Sanitize error if needed
if cfg.SanitizeErrors {
appErr = sanitizeError(appErr).(Error)
}
// Record metrics
if cfg.EnableMetrics {
RecordError(appErr)
}
// Log error
if cfg.LogAllErrors {
log := logger.Default()
log.Error("HTTP error occurred",
logger.String("error_code", appErr.Code()),
logger.String("category", appErr.Category()),
logger.Int("http_status", appErr.HTTPStatus()),
logger.Any("metadata", appErr.Metadata()),
logger.String("method", c.Request.Method),
logger.String("path", c.Request.URL.Path),
logger.String("request_id", c.GetString("request_id")),
)
}
// Create error response
response := createErrorResponse(c, appErr, cfg)
// Send response
c.JSON(appErr.HTTPStatus(), response)
}
// createErrorResponse creates error response
func createErrorResponse(c *gin.Context, err Error, cfg *ErrorMiddlewareConfig) gin.H {
lang := c.GetString("language")
requestID := c.GetString("request_id")
response := gin.H{
"error": gin.H{
"code": err.Code(),
"message": err.GetLocalizedMessage(lang),
"details": err.Metadata(),
"request_id": requestID,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"retryable": IsRetryable(err.Code()),
},
"meta": gin.H{
"http_status": err.HTTPStatus(),
"category": err.Category(),
},
}
// Add stack trace in debug mode
if gin.Mode() == gin.DebugMode {
response["error"].(gin.H)["stack_trace"] = err.Metadata()["stack_trace"]
}
return response
}
// CustomErrorHandler creates custom error handler
func CustomErrorHandler(code string, handler ErrorHandlerFunc) ErrorHandlerFunc {
return func(c *gin.Context, err error) {
appErr := FromError(err)
if appErr != nil && appErr.Code() == code {
handler(c, appErr)
return
}
// Fallback to default handling
HandleHTTPError(c, err)
}
}
// ValidationHandler creates validation error handler
func ValidationHandler(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)
}
// TimeoutHandler creates timeout error handler
func TimeoutHandler(c *gin.Context, err error) {
appErr := NewWithCode(ErrCodeTimeout, "Request timeout").
WithMetadata("timeout", c.GetHeader("X-Timeout")).
WithMetadata("endpoint", c.Request.URL.Path)
HandleHTTPError(c, appErr)
}
// RateLimitHandler creates rate limit error handler
func RateLimitHandler(c *gin.Context, err error) {
appErr := NewWithCode(ErrCodeRateLimitExceeded, "Rate limit exceeded").
WithMetadata("limit", c.GetHeader("X-Rate-Limit-Limit")).
WithMetadata("remaining", c.GetHeader("X-Rate-Limit-Remaining")).
WithMetadata("reset", c.GetHeader("X-Rate-Limit-Reset"))
HandleHTTPError(c, appErr)
}
// NotFoundHandler creates 404 error handler
func NotFoundHandler(c *gin.Context) {
err := NewWithCode(ErrCodeNotFound,
fmt.Sprintf("Route %s %s not found", c.Request.Method, c.Request.URL.Path)).
WithMetadata("method", c.Request.Method).
WithMetadata("path", c.Request.URL.Path).
WithMetadata("query", c.Request.URL.RawQuery)
HandleHTTPError(c, err)
}
// MethodNotAllowedHandler creates 405 error handler
func MethodNotAllowedHandler(c *gin.Context) {
err := NewWithCode(ErrCodeInvalidInput,
fmt.Sprintf("Method %s not allowed for route %s", c.Request.Method, c.Request.URL.Path)).
WithMetadata("method", c.Request.Method).
WithMetadata("path", c.Request.URL.Path).
WithMetadata("allowed_methods", c.GetHeader("Allow"))
HandleHTTPError(c, err)
}
// GRPCErrorInterceptor creates gRPC error interceptor
func GRPCErrorInterceptor(config ...*ErrorMiddlewareConfig) grpc.UnaryServerInterceptor {
cfg := DefaultMiddlewareConfig()
if len(config) > 0 {
cfg = config[0]
}
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err != nil {
return nil, handleGRPCError(err, cfg)
}
return resp, nil
}
}
// handleGRPCError handles gRPC error with configuration
func handleGRPCError(err error, cfg *ErrorMiddlewareConfig) error {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Internal server error")
}
// Apply registered handlers
appErr = ApplyHandlers(appErr)
// Sanitize error if needed
if cfg.SanitizeErrors {
appErr = sanitizeError(appErr).(Error)
}
// Record metrics
if cfg.EnableMetrics {
RecordError(appErr)
}
// Log error
if cfg.LogAllErrors {
log := logger.Default()
log.Error("gRPC error occurred",
logger.String("error_code", appErr.Code()),
logger.String("category", appErr.Category()),
logger.String("grpc_code", appErr.GRPCCode().String()),
logger.Any("metadata", appErr.Metadata()),
)
}
// Convert to gRPC status
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage(cfg.DefaultLanguage))
}
// GetErrorMetrics returns current error metrics
func GetErrorMetrics() ErrorMetrics {
m := GetMetrics()
return ErrorMetrics{
TotalErrors: m.TotalErrors,
ErrorsByCode: m.ErrorsByCode,
ErrorsByCategory: m.ErrorsByCategory,
}
}
// MetricsHandler creates metrics endpoint handler
func MetricsHandler() gin.HandlerFunc {
return func(c *gin.Context) {
metrics := GetErrorMetrics()
c.JSON(http.StatusOK, gin.H{
"error_metrics": metrics,
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
}
// HealthHandler creates health endpoint handler
func HealthHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"service": "error-handler",
})
}
}