| |
| |
| package errors |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| ) |
|
|
| |
| type ErrorCategory int |
|
|
| const ( |
| |
| CategoryUnknown ErrorCategory = iota |
| |
| CategoryAuth |
| |
| CategoryRateLimit |
| |
| CategoryValidation |
| |
| CategoryUpstream |
| |
| CategoryInternal |
| |
| CategoryNetwork |
| |
| CategoryTimeout |
| ) |
|
|
| func (c ErrorCategory) String() string { |
| switch c { |
| case CategoryAuth: |
| return "auth" |
| case CategoryRateLimit: |
| return "rate_limit" |
| case CategoryValidation: |
| return "validation" |
| case CategoryUpstream: |
| return "upstream" |
| case CategoryInternal: |
| return "internal" |
| case CategoryNetwork: |
| return "network" |
| case CategoryTimeout: |
| return "timeout" |
| default: |
| return "unknown" |
| } |
| } |
|
|
| |
| |
| type PipelineError struct { |
| Category ErrorCategory `json:"category"` |
| StatusCode int `json:"status_code"` |
| Message string `json:"message"` |
| Cause error `json:"-"` |
| Retryable bool `json:"retryable"` |
| Context map[string]interface{} `json:"context,omitempty"` |
| } |
|
|
| |
| func (e *PipelineError) Error() string { |
| if e.Cause != nil { |
| return fmt.Sprintf("[%s] %s: %v", e.Category, e.Message, e.Cause) |
| } |
| return fmt.Sprintf("[%s] %s", e.Category, e.Message) |
| } |
|
|
| |
| func (e *PipelineError) Unwrap() error { |
| return e.Cause |
| } |
|
|
| |
| func (e *PipelineError) MarshalJSON() ([]byte, error) { |
| type Alias PipelineError |
| return json.Marshal(&struct { |
| *Alias |
| Cause string `json:"cause,omitempty"` |
| }{ |
| Alias: (*Alias)(e), |
| Cause: func() string { |
| if e.Cause != nil { |
| return e.Cause.Error() |
| } |
| return "" |
| }(), |
| }) |
| } |
|
|
| |
| func (e *PipelineError) WithContext(key string, value interface{}) *PipelineError { |
| if e.Context == nil { |
| e.Context = make(map[string]interface{}) |
| } |
| e.Context[key] = value |
| return e |
| } |
|
|
| |
| func (e *PipelineError) IsRetryable() bool { |
| return e.Retryable |
| } |
|
|
| |
| func New(category ErrorCategory, message string) *PipelineError { |
| return &PipelineError{ |
| Category: category, |
| Message: message, |
| StatusCode: defaultStatusCode(category), |
| Retryable: defaultRetryable(category), |
| } |
| } |
|
|
| |
| func NewWithCause(category ErrorCategory, message string, cause error) *PipelineError { |
| return &PipelineError{ |
| Category: category, |
| Message: message, |
| Cause: cause, |
| StatusCode: defaultStatusCode(category), |
| Retryable: defaultRetryable(category), |
| } |
| } |
|
|
| |
| func Wrap(err error, category ErrorCategory, message string) *PipelineError { |
| if err == nil { |
| return nil |
| } |
|
|
| |
| if pe, ok := err.(*PipelineError); ok { |
| if message != "" { |
| pe.Message = message |
| } |
| if category != CategoryUnknown { |
| pe.Category = category |
| } |
| return pe |
| } |
|
|
| return &PipelineError{ |
| Category: category, |
| Message: message, |
| Cause: err, |
| StatusCode: defaultStatusCode(category), |
| Retryable: defaultRetryable(category), |
| } |
| } |
|
|
| |
|
|
| |
| func AuthError(message string) *PipelineError { |
| return New(CategoryAuth, message) |
| } |
|
|
| |
| func AuthErrorWithCause(message string, cause error) *PipelineError { |
| return NewWithCause(CategoryAuth, message, cause) |
| } |
|
|
| |
|
|
| |
| func RateLimitError(message string) *PipelineError { |
| return New(CategoryRateLimit, message) |
| } |
|
|
| |
| func RateLimitErrorWithRetryAfter(message string, retryAfterSeconds int) *PipelineError { |
| e := New(CategoryRateLimit, message) |
| e.Retryable = true |
| return e.WithContext("retry_after_seconds", retryAfterSeconds) |
| } |
|
|
| |
|
|
| |
| func ValidationError(message string) *PipelineError { |
| return New(CategoryValidation, message) |
| } |
|
|
| |
| func ValidationErrorWithField(message, field string) *PipelineError { |
| return New(CategoryValidation, message).WithContext("field", field) |
| } |
|
|
| |
|
|
| |
| func UpstreamError(message string) *PipelineError { |
| return New(CategoryUpstream, message) |
| } |
|
|
| |
| func UpstreamErrorWithCause(message string, cause error) *PipelineError { |
| return NewWithCause(CategoryUpstream, message, cause) |
| } |
|
|
| |
|
|
| |
| func InternalError(message string) *PipelineError { |
| return New(CategoryInternal, message) |
| } |
|
|
| |
| func InternalErrorWithCause(message string, cause error) *PipelineError { |
| return NewWithCause(CategoryInternal, message, cause) |
| } |
|
|
| |
|
|
| |
| func NetworkError(message string) *PipelineError { |
| return New(CategoryNetwork, message) |
| } |
|
|
| |
| func NetworkErrorWithCause(message string, cause error) *PipelineError { |
| return NewWithCause(CategoryNetwork, message, cause) |
| } |
|
|
| |
|
|
| |
| func TimeoutError(message string) *PipelineError { |
| return New(CategoryTimeout, message) |
| } |
|
|
| |
|
|
| func defaultStatusCode(category ErrorCategory) int { |
| switch category { |
| case CategoryAuth: |
| return 401 |
| case CategoryRateLimit: |
| return 429 |
| case CategoryValidation: |
| return 400 |
| case CategoryUpstream: |
| return 502 |
| case CategoryInternal: |
| return 500 |
| case CategoryNetwork: |
| return 503 |
| case CategoryTimeout: |
| return 504 |
| default: |
| return 500 |
| } |
| } |
|
|
| func defaultRetryable(category ErrorCategory) bool { |
| switch category { |
| case CategoryRateLimit, CategoryNetwork, CategoryTimeout: |
| return true |
| case CategoryAuth, CategoryValidation: |
| return false |
| case CategoryUpstream: |
| return true |
| case CategoryInternal: |
| return false |
| default: |
| return false |
| } |
| } |
|
|
| |
| func IsPipelineError(err error) (*PipelineError, bool) { |
| if err == nil { |
| return nil, false |
| } |
| if pe, ok := err.(*PipelineError); ok { |
| return pe, true |
| } |
| return nil, false |
| } |
|
|
| |
| func IsCategory(err error, category ErrorCategory) bool { |
| if pe, ok := IsPipelineError(err); ok { |
| return pe.Category == category |
| } |
| return false |
| } |
|
|
| |
| func GetCategory(err error) ErrorCategory { |
| if pe, ok := IsPipelineError(err); ok { |
| return pe.Category |
| } |
| return CategoryUnknown |
| } |
|
|
| |
| func IsRetryableError(err error) bool { |
| if pe, ok := IsPipelineError(err); ok { |
| return pe.Retryable |
| } |
| |
| return false |
| } |
|
|
| |
| type ErrorResponse struct { |
| Error string `json:"error"` |
| Category string `json:"category"` |
| StatusCode int `json:"status_code"` |
| Retryable bool `json:"retryable"` |
| Context map[string]interface{} `json:"context,omitempty"` |
| } |
|
|
| |
| func (e *PipelineError) ToResponse() ErrorResponse { |
| return ErrorResponse{ |
| Error: e.Message, |
| Category: e.Category.String(), |
| StatusCode: e.StatusCode, |
| Retryable: e.Retryable, |
| Context: e.Context, |
| } |
| } |
|
|