package errors import ( "crypto/rand" "encoding/hex" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // SecureErrorResponse represents a sanitized error response for clients type SecureErrorResponse struct { Error string `json:"error"` Message string `json:"message"` RequestID string `json:"request_id,omitempty"` Code int `json:"code"` } // ErrorHandler provides secure error handling for HTTP responses type ErrorHandler struct { logger *zap.Logger } // NewErrorHandler creates a new secure error handler func NewErrorHandler(logger *zap.Logger) *ErrorHandler { return &ErrorHandler{ logger: logger, } } // HandleError handles errors securely by logging detailed information and returning sanitized responses func (eh *ErrorHandler) HandleError(c *gin.Context, err error, userMessage string) { requestID := eh.getOrGenerateRequestID(c) // Log detailed error information for internal debugging eh.logger.Error("HTTP request error", zap.String("request_id", requestID), zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), zap.String("user_agent", c.Request.UserAgent()), zap.String("remote_addr", c.ClientIP()), zap.Error(err), ) // Determine appropriate HTTP status code and error type statusCode, errorType := eh.determineErrorResponse(err) // Create sanitized response response := SecureErrorResponse{ Error: errorType, Message: eh.sanitizeErrorMessage(userMessage, err), RequestID: requestID, Code: statusCode, } c.JSON(statusCode, response) } // HandleValidationError handles input validation errors func (eh *ErrorHandler) HandleValidationError(c *gin.Context, field string, message string) { requestID := eh.getOrGenerateRequestID(c) eh.logger.Warn("Validation error", zap.String("request_id", requestID), zap.String("field", field), zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), ) response := SecureErrorResponse{ Error: "validation_error", Message: "Invalid input provided", RequestID: requestID, Code: http.StatusBadRequest, } c.JSON(http.StatusBadRequest, response) } // HandleAuthenticationError handles authentication failures func (eh *ErrorHandler) HandleAuthenticationError(c *gin.Context, err error) { requestID := eh.getOrGenerateRequestID(c) eh.logger.Warn("Authentication error", zap.String("request_id", requestID), zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), zap.String("remote_addr", c.ClientIP()), zap.Error(err), ) response := SecureErrorResponse{ Error: "authentication_failed", Message: "Authentication required", RequestID: requestID, Code: http.StatusUnauthorized, } c.JSON(http.StatusUnauthorized, response) } // HandleAuthorizationError handles authorization failures func (eh *ErrorHandler) HandleAuthorizationError(c *gin.Context, resource string) { requestID := eh.getOrGenerateRequestID(c) eh.logger.Warn("Authorization error", zap.String("request_id", requestID), zap.String("resource", resource), zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), zap.String("remote_addr", c.ClientIP()), ) response := SecureErrorResponse{ Error: "access_denied", Message: "Insufficient permissions", RequestID: requestID, Code: http.StatusForbidden, } c.JSON(http.StatusForbidden, response) } // HandleInternalError handles internal server errors func (eh *ErrorHandler) HandleInternalError(c *gin.Context, err error) { requestID := eh.getOrGenerateRequestID(c) eh.logger.Error("Internal server error", zap.String("request_id", requestID), zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), zap.String("remote_addr", c.ClientIP()), zap.Error(err), ) response := SecureErrorResponse{ Error: "internal_error", Message: "An internal error occurred", RequestID: requestID, Code: http.StatusInternalServerError, } c.JSON(http.StatusInternalServerError, response) } // determineErrorResponse determines the appropriate HTTP status and error type func (eh *ErrorHandler) determineErrorResponse(err error) (int, string) { if appErr, ok := err.(*AppError); ok { return appErr.StatusCode, eh.getErrorTypeFromCode(appErr.Code) } // For unknown errors, log as internal error but don't expose details return http.StatusInternalServerError, "internal_error" } // sanitizeErrorMessage removes sensitive information from error messages func (eh *ErrorHandler) sanitizeErrorMessage(userMessage string, err error) string { if userMessage != "" { return userMessage } // Provide generic messages for different error types if appErr, ok := err.(*AppError); ok { return eh.getGenericMessageFromCode(appErr.Code) } return "An error occurred" } // getErrorTypeFromCode converts an error code to a sanitized error type string func (eh *ErrorHandler) getErrorTypeFromCode(code ErrorCode) string { switch code { case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat: return "validation_error" case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials: return "authentication_failed" case ErrForbidden, ErrInsufficientPermissions: return "access_denied" case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound: return "resource_not_found" case ErrAlreadyExists, ErrConflict: return "resource_conflict" case ErrRateLimit: return "rate_limit_exceeded" case ErrTimeout: return "timeout" default: return "internal_error" } } // getGenericMessageFromCode provides generic user-safe messages for error codes func (eh *ErrorHandler) getGenericMessageFromCode(code ErrorCode) string { switch code { case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat: return "Invalid input provided" case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials: return "Authentication required" case ErrForbidden, ErrInsufficientPermissions: return "Access denied" case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound: return "Resource not found" case ErrAlreadyExists, ErrConflict: return "Resource conflict" case ErrRateLimit: return "Rate limit exceeded" case ErrTimeout: return "Request timeout" default: return "An error occurred" } } // getOrGenerateRequestID gets or generates a request ID for tracking func (eh *ErrorHandler) getOrGenerateRequestID(c *gin.Context) string { // Try to get existing request ID from context if requestID, exists := c.Get("request_id"); exists { if id, ok := requestID.(string); ok { return id } } // Try to get from header requestID := c.GetHeader("X-Request-ID") if requestID != "" { return requestID } // Generate a simple request ID (in production, use a proper UUID library) return generateSimpleID() } // generateSimpleID generates a simple request ID func generateSimpleID() string { // Simple implementation - in production use proper UUID generation bytes := make([]byte, 8) if _, err := rand.Read(bytes); err != nil { // Fallback to timestamp-based ID return fmt.Sprintf("req_%d", time.Now().UnixNano()) } return "req_" + hex.EncodeToString(bytes) }