This commit is contained in:
2025-08-26 19:16:41 -04:00
parent 7ca61eb712
commit 6725529b01
113 changed files with 0 additions and 337 deletions

View File

@ -0,0 +1,360 @@
package errors
import (
"fmt"
"net/http"
)
// ErrorCode represents different types of errors in the system
type ErrorCode string
const (
// Authentication and Authorization errors
ErrUnauthorized ErrorCode = "UNAUTHORIZED"
ErrForbidden ErrorCode = "FORBIDDEN"
ErrInvalidToken ErrorCode = "INVALID_TOKEN"
ErrTokenExpired ErrorCode = "TOKEN_EXPIRED"
ErrInvalidCredentials ErrorCode = "INVALID_CREDENTIALS"
// Validation errors
ErrValidationFailed ErrorCode = "VALIDATION_FAILED"
ErrInvalidInput ErrorCode = "INVALID_INPUT"
ErrMissingField ErrorCode = "MISSING_FIELD"
ErrInvalidFormat ErrorCode = "INVALID_FORMAT"
// Resource errors
ErrNotFound ErrorCode = "NOT_FOUND"
ErrAlreadyExists ErrorCode = "ALREADY_EXISTS"
ErrConflict ErrorCode = "CONFLICT"
// System errors
ErrInternal ErrorCode = "INTERNAL_ERROR"
ErrDatabase ErrorCode = "DATABASE_ERROR"
ErrExternal ErrorCode = "EXTERNAL_SERVICE_ERROR"
ErrTimeout ErrorCode = "TIMEOUT"
ErrRateLimit ErrorCode = "RATE_LIMIT_EXCEEDED"
// Business logic errors
ErrInsufficientPermissions ErrorCode = "INSUFFICIENT_PERMISSIONS"
ErrApplicationNotFound ErrorCode = "APPLICATION_NOT_FOUND"
ErrTokenNotFound ErrorCode = "TOKEN_NOT_FOUND"
ErrPermissionNotFound ErrorCode = "PERMISSION_NOT_FOUND"
ErrInvalidApplication ErrorCode = "INVALID_APPLICATION"
ErrTokenCreationFailed ErrorCode = "TOKEN_CREATION_FAILED"
)
// AppError represents an application error with context
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
StatusCode int `json:"-"`
Internal error `json:"-"`
Context map[string]interface{} `json:"context,omitempty"`
}
// Error implements the error interface
func (e *AppError) Error() string {
if e.Internal != nil {
return fmt.Sprintf("%s: %s (internal: %v)", e.Code, e.Message, e.Internal)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// WithContext adds context information to the error
func (e *AppError) WithContext(key string, value interface{}) *AppError {
if e.Context == nil {
e.Context = make(map[string]interface{})
}
e.Context[key] = value
return e
}
// WithDetails adds additional details to the error
func (e *AppError) WithDetails(details string) *AppError {
e.Details = details
return e
}
// WithInternal adds the underlying error
func (e *AppError) WithInternal(err error) *AppError {
e.Internal = err
return e
}
// New creates a new application error
func New(code ErrorCode, message string) *AppError {
return &AppError{
Code: code,
Message: message,
StatusCode: getHTTPStatusCode(code),
}
}
// Wrap wraps an existing error with application error context
func Wrap(err error, code ErrorCode, message string) *AppError {
return &AppError{
Code: code,
Message: message,
StatusCode: getHTTPStatusCode(code),
Internal: err,
}
}
// getHTTPStatusCode maps error codes to HTTP status codes
func getHTTPStatusCode(code ErrorCode) int {
switch code {
case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials:
return http.StatusUnauthorized
case ErrForbidden, ErrInsufficientPermissions:
return http.StatusForbidden
case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat:
return http.StatusBadRequest
case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound:
return http.StatusNotFound
case ErrAlreadyExists, ErrConflict:
return http.StatusConflict
case ErrRateLimit:
return http.StatusTooManyRequests
case ErrTimeout:
return http.StatusRequestTimeout
case ErrInternal, ErrDatabase, ErrExternal, ErrTokenCreationFailed:
return http.StatusInternalServerError
default:
return http.StatusInternalServerError
}
}
// IsRetryable determines if an error is retryable
func (e *AppError) IsRetryable() bool {
switch e.Code {
case ErrTimeout, ErrExternal, ErrDatabase:
return true
default:
return false
}
}
// IsClientError determines if an error is a client error (4xx)
func (e *AppError) IsClientError() bool {
return e.StatusCode >= 400 && e.StatusCode < 500
}
// IsServerError determines if an error is a server error (5xx)
func (e *AppError) IsServerError() bool {
return e.StatusCode >= 500
}
// Common error constructors for frequently used errors
// NewUnauthorizedError creates an unauthorized error
func NewUnauthorizedError(message string) *AppError {
return New(ErrUnauthorized, message)
}
// NewForbiddenError creates a forbidden error
func NewForbiddenError(message string) *AppError {
return New(ErrForbidden, message)
}
// NewValidationError creates a validation error
func NewValidationError(message string) *AppError {
return New(ErrValidationFailed, message)
}
// NewNotFoundError creates a not found error
func NewNotFoundError(resource string) *AppError {
return New(ErrNotFound, fmt.Sprintf("%s not found", resource))
}
// NewAlreadyExistsError creates an already exists error
func NewAlreadyExistsError(resource string) *AppError {
return New(ErrAlreadyExists, fmt.Sprintf("%s already exists", resource))
}
// NewInternalError creates an internal server error
func NewInternalError(message string) *AppError {
return New(ErrInternal, message)
}
// NewDatabaseError creates a database error
func NewDatabaseError(operation string, err error) *AppError {
return Wrap(err, ErrDatabase, fmt.Sprintf("Database operation failed: %s", operation))
}
// NewTokenError creates a token-related error
func NewTokenError(message string) *AppError {
return New(ErrInvalidToken, message)
}
// NewApplicationError creates an application-related error
func NewApplicationError(message string) *AppError {
return New(ErrInvalidApplication, message)
}
// NewPermissionError creates a permission-related error
func NewPermissionError(message string) *AppError {
return New(ErrInsufficientPermissions, message)
}
// NewAuthenticationError creates an authentication error
func NewAuthenticationError(message string) *AppError {
return New(ErrUnauthorized, message)
}
// NewConfigurationError creates a configuration error
func NewConfigurationError(message string) *AppError {
return New(ErrInternal, message)
}
// ErrorResponse represents the JSON error response format
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code ErrorCode `json:"code"`
Details string `json:"details,omitempty"`
Context map[string]interface{} `json:"context,omitempty"`
}
// ToResponse converts an AppError to an ErrorResponse
func (e *AppError) ToResponse() ErrorResponse {
return ErrorResponse{
Error: string(e.Code),
Message: e.Message,
Code: e.Code,
Details: e.Details,
Context: e.Context,
}
}
// Recovery handles panic recovery and converts to appropriate errors
func Recovery(recovered interface{}) *AppError {
switch v := recovered.(type) {
case *AppError:
return v
case error:
return Wrap(v, ErrInternal, "Internal server error occurred")
case string:
return New(ErrInternal, v)
default:
return New(ErrInternal, "Unknown internal error occurred")
}
}
// Chain represents a chain of errors for better error tracking
type Chain struct {
errors []*AppError
}
// NewChain creates a new error chain
func NewChain() *Chain {
return &Chain{
errors: make([]*AppError, 0),
}
}
// Add adds an error to the chain
func (c *Chain) Add(err *AppError) *Chain {
c.errors = append(c.errors, err)
return c
}
// HasErrors returns true if the chain has any errors
func (c *Chain) HasErrors() bool {
return len(c.errors) > 0
}
// First returns the first error in the chain
func (c *Chain) First() *AppError {
if len(c.errors) == 0 {
return nil
}
return c.errors[0]
}
// Last returns the last error in the chain
func (c *Chain) Last() *AppError {
if len(c.errors) == 0 {
return nil
}
return c.errors[len(c.errors)-1]
}
// All returns all errors in the chain
func (c *Chain) All() []*AppError {
return c.errors
}
// Error implements the error interface for the chain
func (c *Chain) Error() string {
if len(c.errors) == 0 {
return "no errors"
}
if len(c.errors) == 1 {
return c.errors[0].Error()
}
return fmt.Sprintf("multiple errors: %s (and %d more)", c.errors[0].Error(), len(c.errors)-1)
}
// Helper functions to check error types
// IsNotFound checks if an error is a not found error
func IsNotFound(err error) bool {
if appErr, ok := err.(*AppError); ok {
return appErr.Code == ErrNotFound || appErr.Code == ErrApplicationNotFound ||
appErr.Code == ErrTokenNotFound || appErr.Code == ErrPermissionNotFound
}
return false
}
// IsValidationError checks if an error is a validation error
func IsValidationError(err error) bool {
if appErr, ok := err.(*AppError); ok {
return appErr.Code == ErrValidationFailed || appErr.Code == ErrInvalidInput ||
appErr.Code == ErrMissingField || appErr.Code == ErrInvalidFormat
}
return false
}
// IsAuthenticationError checks if an error is an authentication error
func IsAuthenticationError(err error) bool {
if appErr, ok := err.(*AppError); ok {
return appErr.Code == ErrUnauthorized || appErr.Code == ErrInvalidToken ||
appErr.Code == ErrTokenExpired || appErr.Code == ErrInvalidCredentials
}
return false
}
// IsAuthorizationError checks if an error is an authorization error
func IsAuthorizationError(err error) bool {
if appErr, ok := err.(*AppError); ok {
return appErr.Code == ErrForbidden || appErr.Code == ErrInsufficientPermissions
}
return false
}
// IsConflictError checks if an error is a conflict error
func IsConflictError(err error) bool {
if appErr, ok := err.(*AppError); ok {
return appErr.Code == ErrAlreadyExists || appErr.Code == ErrConflict
}
return false
}
// IsInternalError checks if an error is an internal server error
func IsInternalError(err error) bool {
if appErr, ok := err.(*AppError); ok {
return appErr.Code == ErrInternal || appErr.Code == ErrDatabase ||
appErr.Code == ErrExternal || appErr.Code == ErrTokenCreationFailed
}
return false
}
// IsConfigurationError checks if an error is a configuration error
func IsConfigurationError(err error) bool {
if appErr, ok := err.(*AppError); ok {
// Configuration errors are typically mapped to internal errors
return appErr.Code == ErrInternal && appErr.Message != ""
}
return false
}

View File

@ -0,0 +1,267 @@
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)
}
// HandleNotFoundError handles resource not found errors
func (eh *ErrorHandler) HandleNotFoundError(c *gin.Context, resource string, message string) {
requestID := eh.getOrGenerateRequestID(c)
eh.logger.Warn("Resource not found",
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: "resource_not_found",
Message: message,
RequestID: requestID,
Code: http.StatusNotFound,
}
c.JSON(http.StatusNotFound, 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)
}