293 lines
8.2 KiB
Go
293 lines
8.2 KiB
Go
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)
|
|
}
|
|
|
|
// 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)
|
|
}
|