442 lines
11 KiB
Go
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",
|
|
})
|
|
}
|
|
}
|