Files
2026-04-14 01:21:54 +00:00

295 lines
11 KiB
Go

package errors
import (
"net/http"
"strings"
"sync"
"google.golang.org/grpc/codes"
)
// Error categories
const (
CategoryValidation = "validation"
CategoryNotFound = "not_found"
CategoryUnauthorized = "unauthorized"
CategoryForbidden = "forbidden"
CategoryConflict = "conflict"
CategoryRateLimit = "rate_limit"
CategoryInternal = "internal"
CategoryExternal = "external"
CategoryBusiness = "business"
CategoryTimeout = "timeout"
CategoryNetwork = "network"
CategoryDatabase = "database"
)
// Standard error codes
const (
// Validation errors
ErrCodeValidationFailed = "VALIDATION_FAILED"
ErrCodeInvalidInput = "INVALID_INPUT"
ErrCodeMissingField = "MISSING_FIELD"
ErrCodeInvalidFormat = "INVALID_FORMAT"
ErrCodeInvalidLength = "INVALID_LENGTH"
ErrCodeInvalidRange = "INVALID_RANGE"
// Not found errors
ErrCodeNotFound = "NOT_FOUND"
ErrCodeUserNotFound = "USER_NOT_FOUND"
ErrCodeResourceNotFound = "RESOURCE_NOT_FOUND"
ErrCodeDataNotFound = "DATA_NOT_FOUND"
// Authorization errors
ErrCodeUnauthorized = "UNAUTHORIZED"
ErrCodeInvalidToken = "INVALID_TOKEN"
ErrCodeTokenExpired = "TOKEN_EXPIRED"
ErrCodeInvalidCredentials = "INVALID_CREDENTIALS"
// Forbidden errors
ErrCodeForbidden = "FORBIDDEN"
ErrCodeInsufficientRights = "INSUFFICIENT_RIGHTS"
ErrCodeAccessDenied = "ACCESS_DENIED"
// Conflict errors
ErrCodeConflict = "CONFLICT"
ErrCodeDuplicateEntry = "DUPLICATE_ENTRY"
ErrCodeResourceLocked = "RESOURCE_LOCKED"
ErrCodeConcurrentUpdate = "CONCURRENT_UPDATE"
// Rate limit errors
ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
ErrCodeTooManyRequests = "TOO_MANY_REQUESTS"
ErrCodeQuotaExceeded = "QUOTA_EXCEEDED"
// Internal errors
ErrCodeInternalError = "INTERNAL_ERROR"
ErrCodeUnexpectedError = "UNEXPECTED_ERROR"
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
ErrCodeConfigurationError = "CONFIGURATION_ERROR"
// External errors
ErrCodeExternalError = "EXTERNAL_ERROR"
ErrCodeThirdPartyError = "THIRD_PARTY_ERROR"
ErrCodeAPIError = "API_ERROR"
// Business errors
ErrCodeBusinessRule = "BUSINESS_RULE"
ErrCodeInsufficientBalance = "INSUFFICIENT_BALANCE"
ErrCodeAccountSuspended = "ACCOUNT_SUSPENDED"
// Timeout errors
ErrCodeTimeout = "TIMEOUT"
ErrCodeRequestTimeout = "REQUEST_TIMEOUT"
ErrCodeConnectionTimeout = "CONNECTION_TIMEOUT"
// Network errors
ErrCodeNetworkError = "NETWORK_ERROR"
ErrCodeConnectionFailed = "CONNECTION_FAILED"
ErrCodeConnectionLost = "CONNECTION_LOST"
// Database errors
ErrCodeDatabaseError = "DATABASE_ERROR"
ErrCodeQueryFailed = "QUERY_FAILED"
ErrCodeTransactionFailed = "TRANSACTION_FAILED"
)
// ErrorInfo contains error metadata
type ErrorInfo struct {
Code string
Category string
HTTPStatus int
GRPCCode codes.Code
}
// ErrorTemplate for custom error types
type ErrorTemplate struct {
Code string
Message string
Category string
HTTPStatus int
GRPCCode codes.Code
Retryable bool
}
var (
errorCodes = make(map[string]ErrorInfo)
errorCodesMu sync.RWMutex
errorTemplates = make(map[string]*ErrorTemplate)
errorTemplatesMu sync.RWMutex
)
func init() {
// Initialize standard error codes
initializeErrorCodes()
}
// initializeErrorCodes sets up standard error codes
func initializeErrorCodes() {
// Validation errors
registerErrorCode(ErrCodeValidationFailed, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidInput, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeMissingField, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidFormat, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidLength, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidRange, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
// Not found errors
registerErrorCode(ErrCodeNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
registerErrorCode(ErrCodeUserNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
registerErrorCode(ErrCodeResourceNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
registerErrorCode(ErrCodeDataNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
// Authorization errors
registerErrorCode(ErrCodeUnauthorized, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
registerErrorCode(ErrCodeInvalidToken, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
registerErrorCode(ErrCodeTokenExpired, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
registerErrorCode(ErrCodeInvalidCredentials, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
// Forbidden errors
registerErrorCode(ErrCodeForbidden, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
registerErrorCode(ErrCodeInsufficientRights, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
registerErrorCode(ErrCodeAccessDenied, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
// Conflict errors
registerErrorCode(ErrCodeConflict, CategoryConflict, http.StatusConflict, codes.AlreadyExists)
registerErrorCode(ErrCodeDuplicateEntry, CategoryConflict, http.StatusConflict, codes.AlreadyExists)
registerErrorCode(ErrCodeResourceLocked, CategoryConflict, http.StatusLocked, codes.Aborted)
registerErrorCode(ErrCodeConcurrentUpdate, CategoryConflict, http.StatusConflict, codes.Aborted)
// Rate limit errors
registerErrorCode(ErrCodeRateLimitExceeded, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
registerErrorCode(ErrCodeTooManyRequests, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
registerErrorCode(ErrCodeQuotaExceeded, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
// Internal errors
registerErrorCode(ErrCodeInternalError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeUnexpectedError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeServiceUnavailable, CategoryInternal, http.StatusServiceUnavailable, codes.Unavailable)
registerErrorCode(ErrCodeConfigurationError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
// External errors
registerErrorCode(ErrCodeExternalError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeThirdPartyError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeAPIError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
// Business errors
registerErrorCode(ErrCodeBusinessRule, CategoryBusiness, http.StatusBadRequest, codes.FailedPrecondition)
registerErrorCode(ErrCodeInsufficientBalance, CategoryBusiness, http.StatusBadRequest, codes.FailedPrecondition)
registerErrorCode(ErrCodeAccountSuspended, CategoryBusiness, http.StatusForbidden, codes.PermissionDenied)
// Timeout errors
registerErrorCode(ErrCodeTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
registerErrorCode(ErrCodeRequestTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
registerErrorCode(ErrCodeConnectionTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
// Network errors
registerErrorCode(ErrCodeNetworkError, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeConnectionFailed, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeConnectionLost, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
// Database errors
registerErrorCode(ErrCodeDatabaseError, CategoryDatabase, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeQueryFailed, CategoryDatabase, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeTransactionFailed, CategoryDatabase, http.StatusInternalServerError, codes.Aborted)
}
// registerErrorCode registers an error code
func registerErrorCode(code, category string, httpStatus int, grpcCode codes.Code) {
errorCodesMu.Lock()
defer errorCodesMu.Unlock()
errorCodes[code] = ErrorInfo{
Code: code,
Category: category,
HTTPStatus: httpStatus,
GRPCCode: grpcCode,
}
}
// RegisterErrorCode registers a custom error code
func RegisterErrorCode(code, category string, httpStatus int, grpcCode codes.Code) {
registerErrorCode(code, category, httpStatus, grpcCode)
}
// GetErrorInfo returns error information for a code
func GetErrorInfo(code string) ErrorInfo {
errorCodesMu.RLock()
defer errorCodesMu.RUnlock()
if info, exists := errorCodes[code]; exists {
return info
}
return ErrorInfo{
Code: code,
Category: CategoryInternal,
HTTPStatus: http.StatusInternalServerError,
GRPCCode: codes.Internal,
}
}
// RegisterErrorTemplate registers a custom error template
func RegisterErrorTemplate(name string, template *ErrorTemplate) {
errorTemplatesMu.Lock()
defer errorTemplatesMu.Unlock()
errorTemplates[name] = template
}
// GetErrorTemplate returns error template by name
func GetErrorTemplate(name string) *ErrorTemplate {
errorTemplatesMu.RLock()
defer errorTemplatesMu.RUnlock()
return errorTemplates[name]
}
// GetAllErrorCodes returns all registered error codes
func GetAllErrorCodes() map[string]ErrorInfo {
errorCodesMu.RLock()
defer errorCodesMu.RUnlock()
result := make(map[string]ErrorInfo)
for k, v := range errorCodes {
result[k] = v
}
return result
}
// GetErrorCodesByCategory returns error codes by category
func GetErrorCodesByCategory(category string) map[string]ErrorInfo {
errorCodesMu.RLock()
defer errorCodesMu.RUnlock()
result := make(map[string]ErrorInfo)
for k, v := range errorCodes {
if v.Category == category {
result[k] = v
}
}
return result
}
// IsRetryable checks if error is retryable
func IsRetryable(code string) bool {
info := GetErrorInfo(code)
switch info.Category {
case CategoryNetwork, CategoryTimeout, CategoryExternal:
return true
case CategoryDatabase:
return strings.Contains(code, "TIMEOUT") || strings.Contains(code, "CONNECTION")
default:
return false
}
}
// IsClientError checks if error is client-side (4xx)
func IsClientError(code string) bool {
info := GetErrorInfo(code)
return info.HTTPStatus >= 400 && info.HTTPStatus < 500
}
// IsServerError checks if error is server-side (5xx)
func IsServerError(code string) bool {
info := GetErrorInfo(code)
return info.HTTPStatus >= 500
}