Skip to content

错误类型 ​

HTTPC 采用两层错误模型:底层是标准 error 接口,上层是分类的 ClientError 结构体。所有请求失败(网络层)都会被 classifyError 映射为携带上下文的 ClientError,提供错误类型、可重试判断和结构化字段。HTTP 层错误(4xx/5xx)不返回 error,而是通过 Result.StatusCode() 检查。

ClientError ​

go
type ClientError = engine.ClientError

分类的 HTTP 客户端错误,通过 errors.As 提取。是内部 engine.ClientError 的类型别名。

结构体字段 ​

go
type ClientError struct {
    Type       ErrorType  // 错误分类
    Message    string     // 错误描述
    Cause      error      // 底层错误
    URL        string     // 请求 URL(脱敏)
    Method     string     // HTTP 方法
    Attempts   int        // 已尝试次数
    StatusCode int        // HTTP 状态码(如适用)
    Host       string     // 主机名(用于断路器)
}
字段类型说明典型值
ClientError.TypeErrorType错误分类,用于 switch 判断ErrorTypeNetwork、ErrorTypeTimeout
ClientError.Messagestring错误描述信息"network operation failed"
ClientError.Causeerror底层错误,可通过 Unwrap() 获取*net.OpError、*net.DNSError
ClientError.URLstring请求 URL(凭据已脱敏)"https://example.com/path"
ClientError.MethodstringHTTP 方法"GET"、"POST"
ClientError.Attemptsint已尝试次数(含首次请求)1(首次失败)、4(重试 3 次后)
ClientError.StatusCodeintHTTP 状态码(非 HTTP 错误时为 0)0(网络错误)、503(服务端错误)
ClientError.Hoststring请求主机名(用于断路器)"example.com"

其中 Host 供断路器按主机维度统计失败,Message 适合直接进日志,Cause 保留底层错误供 errors.As 深挖。

方法 ​

方法签名说明
Errorfunc (e *ClientError) Error() string格式化为 "METHOD url: message: cause (attempt N)"
Codefunc (e *ClientError) Code() string返回短错误码,如 "NETWORK_ERROR"、"TIMEOUT"
IsRetryablefunc (e *ClientError) IsRetryable() bool判断该错误是否值得重试
Unwrapfunc (e *ClientError) Unwrap() error返回 ClientError.Cause,支持 errors.Is/errors.As 遍历错误链
WithTypefunc (e *ClientError) WithType(t ErrorType) *ClientError返回设置错误类型的副本(不修改原始)

五个方法均以 *ClientError 指针接收者定义:ClientError.Error 实现 error 接口;ClientError.Code 与 ClientError.IsRetryable 供日志分类与重试决策使用;ClientError.Unwrap 暴露底层原因;ClientError.WithType 返回改写类型后的副本,不修改原始错误。

Error() 格式化 ​

Error() 方法将错误格式化为可读字符串:

  • 同时有 URL 和 Method:"GET https://example.com: network operation failed: dial tcp ... (attempt 1)"
  • 仅有 Message:直接输出 Message
  • 有 Cause:追加 ": " + Cause.Error()
  • 有 Attempts(>0):追加 " (attempt N)"

URL 在输出前自动经过内部 validation.SanitizeURL 脱敏(移除凭证)。引擎分类路径产生的错误已预脱敏(urlSanitized=true),跳过冗余的 url.Parse 调用以避免分配。

Code() 错误码 ​

Code() 返回标识错误类型的短字符串,便于日志分类和监控告警:

ErrorTypeCode() 返回值
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(及其他)"UNKNOWN_ERROR"
go
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
    log.Printf("错误码: %s, URL: %s, 尝试次数: %d, 可重试: %v",
        clientErr.Code(), clientErr.URL, clientErr.Attempts, clientErr.IsRetryable())
}

IsRetryable 判断逻辑 ​

IsRetryable() 是 HTTPC 重试机制的核心决策方法。判断流程:

  1. 优先检查上下文错误:若 Cause 是 context.Canceled 或 context.DeadlineExceeded,直接返回 false(永不重试)
  2. 按 ErrorType 分发:
ErrorType是否可重试判断逻辑
ErrorTypeNetwork视情况检查 Cause:包装的 ClientError → 递归判断;*net.OpError → 超时或可重试 syscall(ECONNREFUSED/ECONNRESET/EPIPE/ETIMEDOUT/ENETUNREACH/EHOSTUNREACH);net.Error → 默认可重试;消息匹配("connection reset"/"eof"/"broken pipe" 等)
ErrorTypeTimeout是所有传输层超时均可重试
ErrorTypeTransport是HTTP 传输层错误
ErrorTypeResponseRead视情况仅读操作(Op == "read" 或 "readfrom")可重试;写操作不重试
ErrorTypeDNS视情况Cause 是 *net.DNSError 时,其 IsTemporary 或 IsTimeout 为 true 才重试
ErrorTypeHTTP视情况StatusCode 命中 retryableStatusCodes(408/429/500/502/503/504)可重试
ErrorTypeContextCanceled否用户主动取消
ErrorTypeValidation否请求本身不合法,重试无意义
ErrorTypeTLS否TLS 协议错误,通常不会自愈
ErrorTypeCertificate否证书验证失败,重试无意义
ErrorTypeRetryExhausted否已耗尽重试次数
ErrorTypeUnknown否未知错误,保守不重试

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
}

这是 HTTP 状态码触发重试的唯一真相源,同时被重试逻辑和 IsRetryable() 使用。

超时类型的细微区别

ErrorTypeTimeout 可重试,但由上下文截止触发的超时不可重试——因为 context.DeadlineExceeded 会在第 1 步被拦截(返回 false)。仅传输层超时(如 net.OpError.Timeout())才会到达第 2 步被判定为可重试。这确保用户设置的 WithTimeout 不会被重试突破。

ErrorType ​

go
type ErrorType = engine.ErrorType

错误分类枚举(int 类型)。

常量值含义典型触发场景可重试
ErrorTypeUnknown0未知/未分类无法匹配任何已知模式否
ErrorTypeNetwork1网络层错误连接拒绝、连接重置、网络不可达视情况
ErrorTypeTimeout2超时net.OpError 超时、上下文截止¹视情况²
ErrorTypeContextCanceled3上下文取消context.Cancel 触发否
ErrorTypeResponseRead4响应体读取错误读取响应体时 EOF/连接中断视情况
ErrorTypeTransport5传输层错误HTTP 协议错误、传输失败是
ErrorTypeRetryExhausted6重试耗尽达到 MaxRetries 仍未成功否
ErrorTypeTLS7TLS 错误TLS 握手失败、协议不匹配否
ErrorTypeCertificate8证书验证错误x509 证书过期/不受信任否
ErrorTypeDNS9DNS 解析错误域名不存在、DNS 超时视情况
ErrorTypeValidation10请求验证错误URL 格式错误、重定向超限、CRLF 注入否
ErrorTypeHTTP11HTTP 层错误4xx/5xx 响应(仅重试场景产生)视情况

¹ 由上下文截止(WithTimeout、TimeoutConfig.Request)触发的超时不会重试;仅传输层超时(如 net.OpError 超时)才会重试。 ² 详见上方 IsRetryable 判断逻辑。

类型判断 ​

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("请求超时")
        case httpc.ErrorTypeNetwork:
            log.Println("网络错误:", clientErr.Message)
        case httpc.ErrorTypeTLS:
            log.Println("TLS 错误")
        case httpc.ErrorTypeCertificate:
            log.Println("证书验证失败")
        case httpc.ErrorTypeDNS:
            log.Println("DNS 解析失败")
        case httpc.ErrorTypeRetryExhausted:
            log.Println("重试耗尽,共尝试", clientErr.Attempts, "次")
        case httpc.ErrorTypeContextCanceled:
            log.Println("请求已取消")
        case httpc.ErrorTypeValidation:
            log.Println("请求验证失败")
        }
    }
}

URL 脱敏机制 ​

ClientError.Error() 在格式化时自动调用 validation.SanitizeURL 移除 URL 中的凭证信息(user:pass@host → ***:***@host),防止敏感信息泄漏到日志和错误消息中。

go
// 原始 URL: https://admin:[email protected]/data
// Error() 输出:GET https://***:***@api.example.com/data: ...

引擎分类路径(classifyErrorWithSanitizedURL)在首次分类时即完成脱敏并设置 urlSanitized=true,后续 Error() 调用跳过冗余的 url.Parse,避免每次日志输出都产生分配。

回调中的错误脱敏

MetricsMiddleware 和 LoggingMiddleware 等中间件的回调中,HTTPC 会额外检查非 ClientError 类型的错误消息,将其中的原始 URL 替换为脱敏版本,确保回调不会泄漏凭证。

错误分类流程 ​

classifyError 是将底层 error 映射为 *ClientError 的核心函数,按以下顺序逐层判断:

  1. context 错误:context.Canceled → ErrorTypeContextCanceled;context.DeadlineExceeded → ErrorTypeTimeout
  2. 连接池耗尽:connection.ErrPoolExhausted → ErrorTypeNetwork
  3. *url.Error 解包:HTTP/2 无效头、URL 解析失败 → ErrorTypeValidation;否则解包内层错误继续判断
  4. *net.DNSError:ErrorTypeDNS,区分超时与失败
  5. *net.OpError:ErrorTypeNetwork,区分超时与操作失败
  6. net.Error:超时 → ErrorTypeTimeout;其他 → ErrorTypeNetwork
  7. 消息模式匹配(fallback):按错误消息关键词匹配 TLS/certificate/timeout/connection refused 等 20+ 模式
  8. 兜底:url.Error 的内层未匹配任何模式时 → ErrorTypeNetwork;否则 → ErrorTypeUnknown

错误变量 ​

配置错误 ​

变量错误消息触发条件
ErrNilConfig"config cannot be nil"传入 nil Config 给 New/ValidateConfig
ErrInvalidTimeout"invalid timeout"超时值为负或超过 30 分钟上限
ErrInvalidRetry"invalid retry configuration"MaxRetries 不在 0–10、BackoffFactor 不在 1.0–10.0
ErrInvalidConnection"invalid connection configuration"MaxIdleConns/MaxConnsPerHost 超范围、ProxyURL 格式错误
ErrInvalidSecurity"invalid security configuration"MaxResponseBodySize 超出 0–1GB 范围
ErrInvalidMiddleware"invalid middleware configuration"MaxRedirects 不在 0–50、UserAgent 过长或含控制字符
ErrInvalidHeader"invalid header"请求头键值含控制字符或超过大小限制

请求与响应错误 ​

变量错误消息触发条件
ErrEmptyFilePath"file path cannot be empty"DownloadConfig.FilePath 为空
ErrFileExists"file already exists"文件已存在且 Overwrite=false 且 ResumeDownload=false
ErrResponseBodyEmpty"response body is empty"对空响应体调用 Unmarshal 等解析方法
ErrResponseBodyTooLarge"response body too large"响应体超过 MaxResponseBodySize
ErrInvalidMaxRedirects"invalid max redirects"WithMaxRedirects 传入负数或超过 50
ErrInvalidCredentials"invalid credentials"WithBasicAuth/WithBearerToken 凭据为空、超长或含非法字符
ErrStreamBodyRequiresDownload"WithStreamBody requires Download; standard request methods buffer the response body"WithStreamBody(true) 与标准请求方法(Get/Post/…/Request)搭配——流式仅在 Download 路径生效,其他路径拒绝请求而非静默返回空响应体

下载与代理错误 ​

变量错误消息触发条件
ErrDownloadInProgress"download already in progress for this file path"同一进程内已有针对同一文件路径的下载正在进行——两个并发下载写同一目标会互相破坏,第二个调用方立即失败而非阻塞;首个下载结束后可重试。互斥锁仅在进程内生效,跨进程协调由调用方负责
ErrProxyConnectionFailed"proxy connection failed"配置的代理无法拨号(连接失败、地址非法、被拒绝等)。与底层原因一同包装进错误链,可用 errors.Is(err, httpc.ErrProxyConnectionFailed) 检测;重试引擎也用它判断是否换下一个代理重试

客户端错误 ​

变量错误消息触发条件
ErrClientClosed"client is closed"在 Close() 之后使用客户端

安全警告 API ​

SetSecurityWarnOutput ​

go
func SetSecurityWarnOutput(w io.Writer)

重定向安全警告的输出目标(默认 os.Stderr),定义于 warnings.go。传入 io.Discard 可完全抑制警告。测试环境的判定依据可执行文件名(.test 后缀)与 GO_TEST/GOTEST 环境变量。

两类危险配置会在非测试环境触发警告,每类每进程至多打印一次(sync.Once):

触发条件警告要点
TestingConfig() 创建的客户端用于非测试环境提示 TLS 证书验证、SSRF 防护、URL/Header 验证均被禁用,应改用 SecureConfig() 或 DefaultConfig()
Security.InsecureSkipVerify=true 且非测试环境提示 TLS 证书验证被禁用,仅应在测试中使用,生产应用 SecureConfig()
go
// 抑制安全警告(仅在已确认配置安全时使用)
httpc.SetSecurityWarnOutput(io.Discard)

// 重定向到自定义日志输出
httpc.SetSecurityWarnOutput(log.Writer())

勿滥用抑制

SetSecurityWarnOutput(io.Discard) 会静默吞掉安全警告。仅在已充分审计配置(如确认 TestingConfig 只用于测试二进制)时使用,切勿在生产部署中用它掩盖警告。安全实践的完整清单见安全指南。

实用匹配模式 ​

errors.As 提取 ClientError ​

go
result, err := client.Get(url)
if err != nil {
    var clientErr *httpc.ClientError
    if errors.As(err, &clientErr) {
        // 访问结构化字段
        fmt.Printf("错误码: %s\n", clientErr.Code())
        fmt.Printf("错误类型:%d\n", clientErr.Type)
        fmt.Printf("请求: %s %s\n", clientErr.Method, clientErr.URL)
        fmt.Printf("尝试次数:%d\n", clientErr.Attempts)
        if clientErr.StatusCode != 0 {
            fmt.Printf("状态码:%d\n", clientErr.StatusCode)
        }
    }
}

errors.Is 匹配哨兵错误 ​

go
if errors.Is(err, httpc.ErrClientClosed) {
    // 客户端已关闭,需重新创建
}
if errors.Is(err, httpc.ErrResponseBodyEmpty) {
    // 响应体为空,跳过解析
}
if errors.Is(err, httpc.ErrFileExists) {
    // 文件已存在,提示用户或设置 Overwrite=true
}

errors.Unwrap 遍历错误链 ​

go
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
    // Cause 是底层错误(如 *net.OpError)
    cause := clientErr.Unwrap()
    if cause != nil {
        var opErr *net.OpError
        if errors.As(cause, &opErr) {
            fmt.Println("操作:", opErr.Op)
            fmt.Println("网络:", opErr.Net)
            fmt.Println("地址:", opErr.Addr)
        }
    }
}

三种匹配方式的选择

  • errors.As:需要访问 ClientError 的结构化字段(Type/Code/URL/Attempts 等)
  • errors.Is:匹配哨兵错误(ErrClientClosed 等配置/文件错误)
  • errors.Unwrap:需要到达最底层的 net/error 以获取系统级诊断信息

另见 ​