Error Types
HTTPC uses a two-layer error model: the lower layer is the standard error interface, and the upper layer is the classified ClientError struct. All request failures (network layer) are mapped by classifyError to a context-carrying ClientError, providing an error type, a retryability check, and structured fields. HTTP-layer errors (4xx/5xx) are not returned as errors but checked via Result.StatusCode().
ClientError
type ClientError = engine.ClientErrorClassified HTTP client error, extracted via errors.As. It is a type alias of the internal engine.ClientError.
Struct Fields
type ClientError struct {
Type ErrorType // Error classification
Message string // Error description
Cause error // Underlying error
URL string // Request URL (sanitized)
Method string // HTTP method
Attempts int // Number of attempts made
StatusCode int // HTTP status code (if applicable)
Host string // Hostname (for circuit breakers)
}| Field | Type | Description | Typical value |
|---|---|---|---|
Type | ErrorType | Error classification, for switch branching | ErrorTypeNetwork, ErrorTypeTimeout |
Message | string | Error description | "network operation failed" |
Cause | error | Underlying error, obtainable via Unwrap() | *net.OpError, *net.DNSError |
URL | string | Request URL (credentials sanitized) | "https://example.com/path" |
Method | string | HTTP method | "GET", "POST" |
Attempts | int | Number of attempts (including the first request) | 1 (first failure), 4 (after 3 retries) |
StatusCode | int | HTTP status code (0 for non-HTTP errors) | 0 (network error), 503 (server error) |
Host | string | Request hostname (for circuit breakers) | "example.com" |
Methods
| Method | Return | Description |
|---|---|---|
Error() | string | Formatted as "METHOD url: message: cause (attempt N)" |
Code() | string | Returns a short error code, e.g. "NETWORK_ERROR", "TIMEOUT" |
IsRetryable() | bool | Whether this error is worth retrying |
Unwrap() | error | Returns Cause, supporting errors.Is/errors.As traversal of the error chain |
WithType(t ErrorType) | *ClientError | Returns a copy with the error type set (does not modify the original) |
Error() Formatting
Error() formats the error into a readable string:
- Both URL and Method present:
"GET https://example.com: network operation failed: dial tcp ... (attempt 1)" - Message only: outputs the Message directly
- Has Cause: appends
": " + Cause.Error() - Has Attempts (>0): appends
" (attempt N)"
The URL is automatically sanitized via SanitizeURL (credentials removed) before output. Errors produced by the engine classification path are pre-sanitized (urlSanitized=true), skipping the redundant url.Parse call to avoid allocations.
Code() Error Codes
Code() returns a short string identifying the error type, convenient for log classification and monitoring alerts:
| ErrorType | Code() return value |
|---|---|
ErrorTypeNetwork | "NETWORK_ERROR" |
ErrorTypeTimeout | "TIMEOUT" |
ErrorTypeContextCanceled | "CONTEXT_CANCELED" |
ErrorTypeResponseRead | "RESPONSE_READ_ERROR" |
ErrorTypeTransport | "TRANSPORT_ERROR" |
ErrorTypeRetryExhausted | "RETRY_EXHAUSTED" |
ErrorTypeTLS | "TLS_ERROR" |
ErrorTypeCertificate | "CERTIFICATE_ERROR" |
ErrorTypeDNS | "DNS_ERROR" |
ErrorTypeValidation | "VALIDATION_ERROR" |
ErrorTypeHTTP | "HTTP_ERROR" |
ErrorTypeUnknown (and others) | "UNKNOWN_ERROR" |
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
log.Printf("error code: %s, URL: %s, attempts: %d, retryable: %v",
clientErr.Code(), clientErr.URL, clientErr.Attempts, clientErr.IsRetryable())
}IsRetryable Decision Logic
IsRetryable() is the core decision method for HTTPC's retry mechanism. Decision flow:
- Check context errors first: if Cause is
context.Canceledorcontext.DeadlineExceeded, returns false immediately (never retried) - Dispatch by ErrorType:
| ErrorType | Retryable | Decision logic |
|---|---|---|
ErrorTypeNetwork | Conditional | Inspect Cause: wrapped ClientError -> recursive check; *net.OpError -> timeout or retryable syscall (ECONNREFUSED/ECONNRESET/EPIPE/ETIMEDOUT/ENETUNREACH/EHOSTUNREACH); net.Error -> retryable by default; message match ("connection reset"/"eof"/"broken pipe", etc.) |
ErrorTypeTimeout | Yes | All transport-layer timeouts are retryable |
ErrorTypeTransport | Yes | HTTP transport-layer errors |
ErrorTypeResponseRead | Conditional | Only read operations (Op == "read" or "readfrom") are retryable; write operations are not |
ErrorTypeDNS | Conditional | When Cause is *net.DNSError, retries only if IsTemporary or IsTimeout is true |
ErrorTypeHTTP | Conditional | Retryable when StatusCode hits retryableStatusCodes (408/429/500/502/503/504) |
ErrorTypeContextCanceled | No | User-initiated cancellation |
ErrorTypeValidation | No | The request itself is invalid; retrying is pointless |
ErrorTypeTLS | No | TLS protocol errors usually do not self-heal |
ErrorTypeCertificate | No | Certificate verification failed; retrying is pointless |
ErrorTypeRetryExhausted | No | Retries already exhausted |
ErrorTypeUnknown | No | Unknown error; conservatively not retried |
retryableStatusCodes
var retryableStatusCodes = map[int]bool{
408: true, // Request Timeout
429: true, // Too Many Requests
500: true, // Internal Server Error
502: true, // Bad Gateway
503: true, // Service Unavailable
504: true, // Gateway Timeout
}This is the single source of truth for HTTP-status-code-triggered retries, used by both the retry logic and IsRetryable().
Subtle distinction among timeout types
ErrorTypeTimeout is retryable, but timeouts triggered by a context deadline are not retryable — because context.DeadlineExceeded is intercepted at step 1 (returns false). Only transport-layer timeouts (e.g. net.OpError.Timeout()) reach step 2 and are deemed retryable. This ensures the user's WithTimeout is not breached by retries.
ErrorType
type ErrorType = engine.ErrorTypeError classification enum (type int).
| Constant | Value | Meaning | Typical trigger scenario | Retryable |
|---|---|---|---|---|
ErrorTypeUnknown | 0 | Unknown/unclassified | Matches no known pattern | No |
ErrorTypeNetwork | 1 | Network-layer error | Connection refused, connection reset, network unreachable | Conditional |
ErrorTypeTimeout | 2 | Timeout | net.OpError timeout, context deadline | Conditional |
ErrorTypeContextCanceled | 3 | Context canceled | Triggered by context.Cancel | No |
ErrorTypeResponseRead | 4 | Response-body read error | EOF/connection drop while reading the body | Conditional |
ErrorTypeTransport | 5 | Transport-layer error | HTTP protocol error, transport failure | Yes |
ErrorTypeRetryExhausted | 6 | Retries exhausted | Still failing after reaching MaxRetries | No |
ErrorTypeTLS | 7 | TLS error | TLS handshake failure, protocol mismatch | No |
ErrorTypeCertificate | 8 | Certificate verification error | x509 certificate expired/untrusted | No |
ErrorTypeDNS | 9 | DNS resolution error | Domain does not exist, DNS timeout | Conditional |
ErrorTypeValidation | 10 | Request validation error | Malformed URL, redirect limit exceeded, CRLF injection | No |
ErrorTypeHTTP | 11 | HTTP-layer error | 4xx/5xx response (produced only in retry scenarios) | Conditional |
¹ Timeouts triggered by a context deadline (
WithTimeout,TimeoutConfig.Request) are not retried; only transport-level timeouts (e.g.net.OpErrortimeout) are retried. ² See IsRetryable decision logic above.
Type Checking
result, err := client.Get(url)
if err != nil {
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
switch clientErr.Type {
case httpc.ErrorTypeTimeout:
log.Println("request timed out")
case httpc.ErrorTypeNetwork:
log.Println("network error:", clientErr.Message)
case httpc.ErrorTypeTLS:
log.Println("TLS error")
case httpc.ErrorTypeCertificate:
log.Println("certificate verification failed")
case httpc.ErrorTypeDNS:
log.Println("DNS resolution failed")
case httpc.ErrorTypeRetryExhausted:
log.Println("retries exhausted, attempted", clientErr.Attempts, "times")
case httpc.ErrorTypeContextCanceled:
log.Println("request canceled")
case httpc.ErrorTypeValidation:
log.Println("request validation failed")
}
}
}URL Sanitization Mechanism
ClientError.Error() automatically calls validation.SanitizeURL when formatting to remove credentials from the URL (user:pass@host -> ***:***@host), preventing sensitive information from leaking into logs and error messages.
// Original URL: https://admin:[email protected]/data
// Error() output: GET https://***:***@api.example.com/data: ...The engine classification path (classifyErrorWithSanitizedURL) performs sanitization at first classification and sets urlSanitized=true; subsequent Error() calls skip the redundant url.Parse to avoid allocations on every log output.
Error sanitization in callbacks
In callbacks for middleware such as MetricsMiddleware and LoggingMiddleware, HTTPC additionally checks error messages of non-ClientError errors, replacing the original URL with a sanitized version to ensure callbacks never leak credentials.
Error Classification Flow
classifyError is the core function that maps an underlying error to a *ClientError, checking layer by layer in this order:
- Context errors:
context.Canceled->ErrorTypeContextCanceled;context.DeadlineExceeded->ErrorTypeTimeout - Connection-pool exhaustion:
connection.ErrPoolExhausted->ErrorTypeNetwork *url.Errorunwrap: invalid HTTP/2 header, URL parse failure ->ErrorTypeValidation; otherwise unwrap the inner error and continue*net.DNSError:ErrorTypeDNS, distinguishing timeout from failure*net.OpError:ErrorTypeNetwork, distinguishing timeout from operation failurenet.Error: timeout ->ErrorTypeTimeout; other ->ErrorTypeNetwork- Message-pattern matching (fallback): matches 20+ patterns by keywords in the error message (TLS/certificate/timeout/connection refused, etc.)
- Fallback: when the inner of a
url.Errormatches no pattern ->ErrorTypeNetwork; otherwise ->ErrorTypeUnknown
Error Variables
Configuration Errors
| Variable | Error message | Trigger condition |
|---|---|---|
ErrNilConfig | "config cannot be nil" | nil Config passed to New/ValidateConfig |
ErrInvalidTimeout | "invalid timeout" | Timeout value is negative or exceeds the 30-minute cap |
ErrInvalidRetry | "invalid retry configuration" | MaxRetries outside 0-10, BackoffFactor outside 1.0-10.0 |
ErrInvalidConnection | "invalid connection configuration" | MaxIdleConns/MaxConnsPerHost out of range, malformed ProxyURL |
ErrInvalidSecurity | "invalid security configuration" | MaxResponseBodySize outside the 0-1GB range |
ErrInvalidMiddleware | "invalid middleware configuration" | MaxRedirects outside 0-50, UserAgent too long or contains control characters |
ErrInvalidHeader | "invalid header" | Header key-value contains control characters or exceeds the size limit |
Request and Response Errors
| Variable | Error message | Trigger condition |
|---|---|---|
ErrEmptyFilePath | "file path cannot be empty" | DownloadConfig.FilePath is empty |
ErrFileExists | "file already exists" | File exists and Overwrite=false and ResumeDownload=false |
ErrResponseBodyEmpty | "response body is empty" | Calling Unmarshal or similar parsing methods on an empty body |
ErrResponseBodyTooLarge | "response body too large" | Response body exceeds MaxResponseBodySize |
Client Errors
| Variable | Error message | Trigger condition |
|---|---|---|
ErrClientClosed | "client is closed" | Using the client after Close() |
Practical Matching Patterns
errors.As to Extract ClientError
result, err := client.Get(url)
if err != nil {
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
// Access structured fields
fmt.Printf("error code: %s\n", clientErr.Code())
fmt.Printf("error type: %d\n", clientErr.Type)
fmt.Printf("request: %s %s\n", clientErr.Method, clientErr.URL)
fmt.Printf("attempts: %d\n", clientErr.Attempts)
if clientErr.StatusCode != 0 {
fmt.Printf("status code: %d\n", clientErr.StatusCode)
}
}
}errors.Is to Match Sentinel Errors
if errors.Is(err, httpc.ErrClientClosed) {
// Client is closed; recreate it
}
if errors.Is(err, httpc.ErrResponseBodyEmpty) {
// Response body is empty; skip parsing
}
if errors.Is(err, httpc.ErrFileExists) {
// File already exists; prompt the user or set Overwrite=true
}errors.Unwrap to Traverse the Error Chain
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
// Cause is the underlying error (e.g. *net.OpError)
cause := clientErr.Unwrap()
if cause != nil {
var opErr *net.OpError
if errors.As(cause, &opErr) {
fmt.Println("operation:", opErr.Op)
fmt.Println("network:", opErr.Net)
fmt.Println("address:", opErr.Addr)
}
}
}Choosing among the three matching styles
errors.As: when you need to access ClientError's structured fields (Type/Code/URL/Attempts, etc.)errors.Is: to match sentinel errors (config/file errors like ErrClientClosed)errors.Unwrap: when you need to reach the bottom-level net/error for system-level diagnostics
See Also
- Error Handling - Complete error handling guide
- Constants and Types - BodyKind and other constants reference
- Retry and Fault Tolerance - Retry strategy guide