package fetcher import ( "errors" "fmt" "net" "net/url" "strings" ) type FetchError struct { StatusCode int UserMessage string Retryable bool UnderlyingError error } func (fetchError *FetchError) Error() string { if fetchError.UnderlyingError != nil { return fmt.Sprintf("%s: %v", fetchError.UserMessage, fetchError.UnderlyingError) } return fetchError.UserMessage } func (fetchError *FetchError) Unwrap() error { return fetchError.UnderlyingError } func ClassifyError(originalError error, statusCode int) *FetchError { if statusCode >= 400 { return classifyHTTPStatusCode(statusCode, originalError) } return classifyNetworkError(originalError) } func classifyHTTPStatusCode(statusCode int, originalError error) *FetchError { switch statusCode { case 401: return &FetchError{ StatusCode: statusCode, UserMessage: "authentication required - check feed credentials", Retryable: false, UnderlyingError: originalError, } case 403: return &FetchError{ StatusCode: statusCode, UserMessage: "access forbidden - feed may require different credentials", Retryable: false, UnderlyingError: originalError, } case 404: return &FetchError{ StatusCode: statusCode, UserMessage: "feed not found - URL may be incorrect or feed removed", Retryable: false, UnderlyingError: originalError, } case 410: return &FetchError{ StatusCode: statusCode, UserMessage: "feed permanently removed", Retryable: false, UnderlyingError: originalError, } case 429: return &FetchError{ StatusCode: statusCode, UserMessage: "rate limited by feed server - will retry later", Retryable: true, UnderlyingError: originalError, } case 500, 502, 503, 504: return &FetchError{ StatusCode: statusCode, UserMessage: fmt.Sprintf("feed server error (HTTP %d) - will retry later", statusCode), Retryable: true, UnderlyingError: originalError, } default: return &FetchError{ StatusCode: statusCode, UserMessage: fmt.Sprintf("unexpected HTTP status %d", statusCode), Retryable: statusCode >= 500, UnderlyingError: originalError, } } } func classifyNetworkError(originalError error) *FetchError { if originalError == nil { return &FetchError{ UserMessage: "unknown fetch error", Retryable: true, } } var dnsError *net.DNSError if errors.As(originalError, &dnsError) { return &FetchError{ UserMessage: "DNS resolution failed - check feed URL", Retryable: !dnsError.IsNotFound, UnderlyingError: originalError, } } var urlError *url.Error if errors.As(originalError, &urlError) { if urlError.Timeout() { return &FetchError{ UserMessage: "connection timed out - feed server may be slow", Retryable: true, UnderlyingError: originalError, } } } var netOpError *net.OpError if errors.As(originalError, &netOpError) { return &FetchError{ UserMessage: "network connection error - will retry later", Retryable: true, UnderlyingError: originalError, } } if strings.Contains(originalError.Error(), "certificate") || strings.Contains(originalError.Error(), "tls") { return &FetchError{ UserMessage: "TLS/certificate error - feed server has invalid certificate", Retryable: false, UnderlyingError: originalError, } } return &FetchError{ UserMessage: "unexpected network error", Retryable: true, UnderlyingError: originalError, } }