Skip to content

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 ​

go
type ClientError = engine.ClientError

Classified HTTP client error, extracted via errors.As. It is a type alias of the internal engine.ClientError.

Struct Fields ​

go
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)
}
FieldTypeDescriptionTypical value
ClientError.TypeErrorTypeError classification, for switch branchingErrorTypeNetwork, ErrorTypeTimeout
ClientError.MessagestringError description"network operation failed"
ClientError.CauseerrorUnderlying error, obtainable via Unwrap()*net.OpError, *net.DNSError
ClientError.URLstringRequest URL (credentials sanitized)"https://example.com/path"
ClientError.MethodstringHTTP method"GET", "POST"
ClientError.AttemptsintNumber of attempts (including the first request)1 (first failure), 4 (after 3 retries)
ClientError.StatusCodeintHTTP status code (0 for non-HTTP errors)0 (network error), 503 (server error)
ClientError.HoststringRequest hostname (for circuit breakers)"example.com"

Among them, Host lets circuit breakers track failures per hostname, Message is suitable for direct logging, and Cause preserves the underlying error for errors.As inspection.

Methods ​

MethodSignatureDescription
Errorfunc (e *ClientError) Error() stringFormatted as "METHOD url: message: cause (attempt N)"
Codefunc (e *ClientError) Code() stringReturns a short error code, e.g. "NETWORK_ERROR", "TIMEOUT"
IsRetryablefunc (e *ClientError) IsRetryable() boolWhether this error is worth retrying
Unwrapfunc (e *ClientError) Unwrap() errorReturns ClientError.Cause, supporting errors.Is/errors.As traversal of the error chain
WithTypefunc (e *ClientError) WithType(t ErrorType) *ClientErrorReturns a copy with the error type set (does not modify the original)

All five methods are defined on the *ClientError pointer receiver: ClientError.Error implements the error interface; ClientError.Code and ClientError.IsRetryable serve log classification and retry decisions; ClientError.Unwrap exposes the underlying cause; ClientError.WithType returns a copy with the type rewritten, leaving the original error untouched.

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 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:

ErrorTypeCode() 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"
go
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:

  1. Check context errors first: if Cause is context.Canceled or context.DeadlineExceeded, returns false immediately (never retried)
  2. Dispatch by ErrorType:
ErrorTypeRetryableDecision logic
ErrorTypeNetworkConditionalInspect 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.)
ErrorTypeTimeoutYesAll transport-layer timeouts are retryable
ErrorTypeTransportYesHTTP transport-layer errors
ErrorTypeResponseReadConditionalOnly read operations (Op == "read" or "readfrom") are retryable; write operations are not
ErrorTypeDNSConditionalWhen Cause is *net.DNSError, retries only if IsTemporary or IsTimeout is true
ErrorTypeHTTPConditionalRetryable when StatusCode hits retryableStatusCodes (408/429/500/502/503/504)
ErrorTypeContextCanceledNoUser-initiated cancellation
ErrorTypeValidationNoThe request itself is invalid; retrying is pointless
ErrorTypeTLSNoTLS protocol errors usually do not self-heal
ErrorTypeCertificateNoCertificate verification failed; retrying is pointless
ErrorTypeRetryExhaustedNoRetries already exhausted
ErrorTypeUnknownNoUnknown error; conservatively not retried

retryableStatusCodes ​

go
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 ​

go
type ErrorType = engine.ErrorType

Error classification enum (type int).

ConstantValueMeaningTypical trigger scenarioRetryable
ErrorTypeUnknown0Unknown/unclassifiedMatches no known patternNo
ErrorTypeNetwork1Network-layer errorConnection refused, connection reset, network unreachableConditional
ErrorTypeTimeout2Timeoutnet.OpError timeout, context deadlineConditional
ErrorTypeContextCanceled3Context canceledTriggered by context.CancelNo
ErrorTypeResponseRead4Response-body read errorEOF/connection drop while reading the bodyConditional
ErrorTypeTransport5Transport-layer errorHTTP protocol error, transport failureYes
ErrorTypeRetryExhausted6Retries exhaustedStill failing after reaching MaxRetriesNo
ErrorTypeTLS7TLS errorTLS handshake failure, protocol mismatchNo
ErrorTypeCertificate8Certificate verification errorx509 certificate expired/untrustedNo
ErrorTypeDNS9DNS resolution errorDomain does not exist, DNS timeoutConditional
ErrorTypeValidation10Request validation errorMalformed URL, redirect limit exceeded, CRLF injectionNo
ErrorTypeHTTP11HTTP-layer error4xx/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.OpError timeout) are retried. ² See IsRetryable decision logic above.

Type Checking ​

go
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.

go
// 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:

  1. Context errors: context.Canceled -> ErrorTypeContextCanceled; context.DeadlineExceeded -> ErrorTypeTimeout
  2. Connection-pool exhaustion: connection.ErrPoolExhausted -> ErrorTypeNetwork
  3. *url.Error unwrap: invalid HTTP/2 header, URL parse failure -> ErrorTypeValidation; otherwise unwrap the inner error and continue
  4. *net.DNSError: ErrorTypeDNS, distinguishing timeout from failure
  5. *net.OpError: ErrorTypeNetwork, distinguishing timeout from operation failure
  6. net.Error: timeout -> ErrorTypeTimeout; other -> ErrorTypeNetwork
  7. Message-pattern matching (fallback): matches 20+ patterns by keywords in the error message (TLS/certificate/timeout/connection refused, etc.)
  8. Fallback: when the inner of a url.Error matches no pattern -> ErrorTypeNetwork; otherwise -> ErrorTypeUnknown

Error Variables ​

Configuration Errors ​

VariableError messageTrigger 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 ​

VariableError messageTrigger 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
ErrInvalidMaxRedirects"invalid max redirects"WithMaxRedirects passed a negative value or one exceeding 50
ErrInvalidCredentials"invalid credentials"WithBasicAuth/WithBearerToken credentials empty, oversized, or containing invalid characters
ErrStreamBodyRequiresDownload"WithStreamBody requires Download; standard request methods buffer the response body"WithStreamBody(true) combined with the standard request methods (Get/Post/.../Request) — streaming only takes effect on the Download path; every other path rejects the request instead of silently returning an empty body

Download and Proxy Errors ​

VariableError messageTrigger condition
ErrDownloadInProgress"download already in progress for this file path"A download targeting the same file path is already running in this process — two concurrent downloads to one destination would corrupt each other's writes, so the second caller fails fast instead of blocking; retry once the first download finishes. The mutex is in-process only; coordinating downloads across separate processes is the caller's responsibility
ErrProxyConnectionFailed"proxy connection failed"A configured proxy could not be dialed (dial failure, invalid address, refused connection, etc.). Wrapped alongside the underlying cause in the error chain and detectable with errors.Is(err, httpc.ErrProxyConnectionFailed); the retry engine also uses it to decide whether to retry with the next proxy in the pool

Client Errors ​

VariableError messageTrigger condition
ErrClientClosed"client is closed"Using the client after Close()

Security Warning API ​

SetSecurityWarnOutput ​

go
func SetSecurityWarnOutput(w io.Writer)

Redirects the output target of security warnings (default os.Stderr); defined in warnings.go. Passing io.Discard suppresses the warnings entirely. Test environments are detected by the executable file name (.test suffix) and the GO_TEST/GOTEST environment variables.

Two categories of dangerous configuration trigger warnings in non-test environments, each printed at most once per process (sync.Once):

Trigger conditionWarning summary
A client created with TestingConfig() is used outside a test environmentPoints out that TLS certificate verification, SSRF protection, and URL/Header validation are all disabled, and recommends SecureConfig() or DefaultConfig() instead
Security.InsecureSkipVerify=true in a non-test environmentPoints out that TLS certificate verification is disabled, that it should only be used in tests, and that production should use SecureConfig()
go
// Suppress security warnings (only use after confirming the configuration is safe)
httpc.SetSecurityWarnOutput(io.Discard)

// Redirect to a custom log output
httpc.SetSecurityWarnOutput(log.Writer())

Don't overuse suppression

SetSecurityWarnOutput(io.Discard) silently swallows security warnings. Use it only after fully auditing your configuration (e.g. confirming that TestingConfig is only used in test binaries), and never to mask warnings in a production deployment. For the complete list of security practices, see the Security Overview.

Practical Matching Patterns ​

errors.As to Extract ClientError ​

go
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 ​

go
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 ​

go
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 ​