200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
package laozhang
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Sentinel errors for common error types
|
|
var (
|
|
// ErrInvalidConfig indicates configuration validation failed
|
|
ErrInvalidConfig = errors.New("invalid configuration")
|
|
|
|
// ErrRateLimit indicates rate limit exceeded (HTTP 429)
|
|
ErrRateLimit = errors.New("rate limit exceeded")
|
|
|
|
// ErrServerError indicates server-side error (HTTP 5xx)
|
|
ErrServerError = errors.New("server error")
|
|
|
|
// ErrInvalidRequest indicates client error (HTTP 4xx except 429, 401)
|
|
ErrInvalidRequest = errors.New("invalid request")
|
|
|
|
// ErrTimeout indicates request timeout
|
|
ErrTimeout = errors.New("request timeout")
|
|
|
|
// ErrUnauthorized indicates authentication failed (HTTP 401)
|
|
ErrUnauthorized = errors.New("unauthorized")
|
|
)
|
|
|
|
// APIError represents an error returned by the LaoZhang API
|
|
type APIError struct {
|
|
StatusCode int // HTTP status code
|
|
Message string // Human-readable error message
|
|
Type string // Error type from API response
|
|
Code string // Error code from API response
|
|
err error // Underlying error for wrapping
|
|
Details map[string]string // Additional error details
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *APIError) Error() string {
|
|
if e.Code != "" {
|
|
return fmt.Sprintf("laozhang api error (status %d): [%s] %s", e.StatusCode, e.Code, e.Message)
|
|
}
|
|
return fmt.Sprintf("laozhang api error (status %d): %s", e.StatusCode, e.Message)
|
|
}
|
|
|
|
// Unwrap implements the errors.Unwrap interface for error chains
|
|
func (e *APIError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
// NewAPIError creates a new APIError with the given parameters
|
|
func NewAPIError(statusCode int, message, errorType, code string, underlying error) *APIError {
|
|
return &APIError{
|
|
StatusCode: statusCode,
|
|
Message: message,
|
|
Type: errorType,
|
|
Code: code,
|
|
err: underlying,
|
|
Details: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// WithDetails adds additional details to the error
|
|
func (e *APIError) WithDetails(key, value string) *APIError {
|
|
if e.Details == nil {
|
|
e.Details = make(map[string]string)
|
|
}
|
|
e.Details[key] = value
|
|
return e
|
|
}
|
|
|
|
// IsRateLimitError checks if the error is a rate limit error
|
|
func IsRateLimitError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for sentinel error
|
|
if errors.Is(err, ErrRateLimit) {
|
|
return true
|
|
}
|
|
|
|
// Check for APIError with 429 status
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
return apiErr.StatusCode == 429
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsServerError checks if the error is a server error (5xx)
|
|
func IsServerError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for sentinel error
|
|
if errors.Is(err, ErrServerError) {
|
|
return true
|
|
}
|
|
|
|
// Check for APIError with 5xx status
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
return apiErr.StatusCode >= 500 && apiErr.StatusCode < 600
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsRetryableError checks if the error should trigger a retry
|
|
// Retryable errors include: rate limits (429), server errors (5xx), and timeouts
|
|
func IsRetryableError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check sentinel errors
|
|
if errors.Is(err, ErrRateLimit) || errors.Is(err, ErrServerError) || errors.Is(err, ErrTimeout) {
|
|
return true
|
|
}
|
|
|
|
// Check APIError status codes
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
// Retry on rate limits and server errors
|
|
return apiErr.StatusCode == 429 || (apiErr.StatusCode >= 500 && apiErr.StatusCode < 600)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsUnauthorizedError checks if the error is an unauthorized error (401)
|
|
func IsUnauthorizedError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for sentinel error
|
|
if errors.Is(err, ErrUnauthorized) {
|
|
return true
|
|
}
|
|
|
|
// Check for APIError with 401 status
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
return apiErr.StatusCode == 401
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsTimeoutError checks if the error is a timeout error
|
|
func IsTimeoutError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
return errors.Is(err, ErrTimeout)
|
|
}
|
|
|
|
// IsInvalidRequestError checks if the error is a client error (4xx except 429, 401)
|
|
func IsInvalidRequestError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for sentinel error
|
|
if errors.Is(err, ErrInvalidRequest) {
|
|
return true
|
|
}
|
|
|
|
// Check for APIError with 4xx status (excluding 429 and 401)
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
return apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 &&
|
|
apiErr.StatusCode != 429 && apiErr.StatusCode != 401
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// classifyHTTPError classifies an HTTP status code into a domain error type
|
|
func classifyHTTPError(statusCode int) error {
|
|
switch {
|
|
case statusCode == 401:
|
|
return ErrUnauthorized
|
|
case statusCode == 429:
|
|
return ErrRateLimit
|
|
case statusCode >= 400 && statusCode < 500:
|
|
return ErrInvalidRequest
|
|
case statusCode >= 500 && statusCode < 600:
|
|
return ErrServerError
|
|
default:
|
|
return nil
|
|
}
|
|
}
|