Files
skybridge/kms/internal/errors/errors.go
2025-08-26 19:16:41 -04:00

361 lines
10 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)
}
// 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
}