// HTTP error classification -- the protocol-neutral DefaultClassifier (pure // HTTP status-code mapping) plus connection-error diagnosis. Kept in the // apierror package so both the wire path and the transport path share one // classifier without importing each other; provider-specific classifiers // (e.g. AnthropicClassifier) stay in the transport layer and delegate here. // // HTTP 错误分类 -- 协议无关的 DefaultClassifier (纯 HTTP 状态码映射) 加连接 // 错误诊断. 放在 apierror 包, 使 wire 路径和 transport 路径共用同一套分类器 // 而不互相 import; 供应商特定分类器 (如 AnthropicClassifier) 留在 transport // 层并委托到这里. package apierror import ( "errors" "net" "net/http" "strings" ) // ============================================================ // DefaultClassifier - 纯 HTTP 状态码分类 // ============================================================ // DefaultClassifier 是基于 HTTP 状态码的默认分类器. // 不解析任何供应商特定的 header 或 body 内容,作为兜底分类器使用. type DefaultClassifier struct{} // Classify 基于 HTTP 状态码分类. func (c *DefaultClassifier) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError { // 连接错误(statusCode == 0 表示未收到 HTTP 响应) if cause != nil && statusCode == 0 { return classifyConnectionError(cause) } errType, message := ParseAPIErrorBody(body) if message == "" { message = string(body) } apiErr := &APIError{ StatusCode: statusCode, Msg: message, RespHeaders: headers, Body: string(body), Cause: cause, } // 状态码分类 switch { case statusCode == 400: apiErr.ErrCategory = classifyBadRequest(errType, message) case statusCode == 401 || statusCode == 403: apiErr.ErrCategory = ErrAuthentication case statusCode == 404: apiErr.ErrCategory = ErrModelNotFound case statusCode == 408: apiErr.ErrCategory = ErrTimeout apiErr.Retry = &RetryInfo{Retryable: true} case statusCode == 413: apiErr.ErrCategory = ErrRequestTooLarge case statusCode == 429: apiErr.ErrCategory = ErrRateLimit apiErr.Retry = parseRetryInfoFromHeaders(headers) case statusCode == 529: apiErr.ErrCategory = ErrOverloaded apiErr.Retry = &RetryInfo{Retryable: true} case statusCode == 507: // 507 Insufficient Storage (RFC 4918): 资源/存储不足, 语义上非瞬态 -- // 重试同样不足, 保守默认不可重试 (nil Retry 回落 ErrRequestTooLarge // IsRetryableByDefault=false). 自托管推理后端 (如 fmlx) 用 507 表示 // "模型永久太大装不下"的永久错误; 瞬态资源紧张应走 503 + Retry-After // (下方 >= 500 分支尊重 Retry-After). 必须放在 >= 500 之前才命中. // // 507 Insufficient Storage: resource shortage is non-transient by // semantics -- retrying finds the same shortage. Reserved for the // permanent "won't fit" case; transient pressure should use 503. apiErr.ErrCategory = ErrRequestTooLarge case statusCode >= 500: apiErr.ErrCategory = ErrServerError // 5xx 默认可重试; 尊重服务端 Retry-After (503 "服务暂时不可用, 稍后 // 重试"是标准语义) 与 x-should-retry, 缺省则调用方指数退避. 复用 429 // 同款 header 解析 (无 header 仍 Retryable:true, 不破坏 5xx 默认). // // 5xx defaults retryable; honor server Retry-After (503 + Retry-After // is the standard "unavailable, retry later" signal) and x-should-retry. apiErr.Retry = parseRetryInfoFromHeaders(headers) default: apiErr.ErrCategory = ErrUnknown } return apiErr } // classifyBadRequest 细分 400 错误的子类型. func classifyBadRequest(errType, message string) ErrorCategory { lower := strings.ToLower(message) // 过载错误(SDK 流式时有时把 529 报为 400) if errType == "overloaded_error" || strings.Contains(message, `"type":"overloaded_error"`) { return ErrOverloaded } // Prompt 太长 if strings.Contains(lower, "prompt is too long") { return ErrPromptTooLong } // 媒体大小限制 if (strings.Contains(message, "image exceeds") && strings.Contains(message, "maximum")) || (strings.Contains(message, "image dimensions exceed") && strings.Contains(message, "many-image")) { return ErrMediaTooLarge } if matchesPDFPageLimit(message) { return ErrMediaTooLarge } // Tool 相关错误 if strings.Contains(message, "tool_use` ids were found without `tool_result`") { return ErrToolMismatch } if strings.Contains(message, "unexpected `tool_use_id` found in `tool_result`") { return ErrUnexpectedTool } if strings.Contains(message, "`tool_use` ids must be unique") { return ErrDuplicateToolID } // 模型名称无效 if strings.Contains(lower, "invalid model name") { return ErrInvalidModel } // 余额不足 if strings.Contains(lower, "credit balance is too low") { return ErrBilling } return ErrInvalidRequest } // matchesPDFPageLimit 检查是否是 PDF 页数超限错误. func matchesPDFPageLimit(message string) bool { return strings.Contains(message, "maximum of") && strings.Contains(message, "PDF pages") } // ============================================================ // 辅助:从 HTTP 响应头解析重试信息 // ============================================================ // parseRetryInfoFromHeaders 从标准 HTTP header 解析重试信息. func parseRetryInfoFromHeaders(headers http.Header) *RetryInfo { if headers == nil { return &RetryInfo{Retryable: true} // 429 默认可重试 } info := &RetryInfo{Retryable: true} // 标准 retry-after header if ra := headers.Get("Retry-After"); ra != "" { info.After = ParseRetryAfter(ra) } // x-should-retry header if shouldRetry := ParseShouldRetry(headers.Get("x-should-retry")); shouldRetry != nil { info.ServerSaid = shouldRetry info.Retryable = *shouldRetry } return info } // ============================================================ // SSL 错误码目录 // ============================================================ // sslErrorCodes 是 OpenSSL 定义的 SSL/TLS 错误码集合. // 参考: https://www.openssl.org/docs/man3.1/man3/X509_STORE_CTX_get_error.html // // 精妙之处(CLEVER): 用 map[string]struct{} 而非 []string-- // 查找是 O(1) 而非 O(n),19 个元素差距不大但习惯好. var sslErrorCodes = map[string]struct{}{ // 证书验证错误 "UNABLE_TO_VERIFY_LEAF_SIGNATURE": {}, "UNABLE_TO_GET_ISSUER_CERT": {}, "UNABLE_TO_GET_ISSUER_CERT_LOCALLY": {}, "CERT_SIGNATURE_FAILURE": {}, "CERT_NOT_YET_VALID": {}, "CERT_HAS_EXPIRED": {}, "CERT_REVOKED": {}, "CERT_REJECTED": {}, "CERT_UNTRUSTED": {}, // 自签名证书 "DEPTH_ZERO_SELF_SIGNED_CERT": {}, "SELF_SIGNED_CERT_IN_CHAIN": {}, // 证书链错误 "CERT_CHAIN_TOO_LONG": {}, "PATH_LENGTH_EXCEEDED": {}, // 主机名/备用名错误 "ERR_TLS_CERT_ALTNAME_INVALID": {}, "HOSTNAME_MISMATCH": {}, // TLS 握手错误 "ERR_TLS_HANDSHAKE_TIMEOUT": {}, "ERR_SSL_WRONG_VERSION_NUMBER": {}, "ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC": {}, // Go crypto/tls 特有 "tls: failed to verify certificate": {}, } // IsSSLErrorCode 检查给定的错误码或消息片段是否为 SSL/TLS 相关. func IsSSLErrorCode(code string) bool { if _, ok := sslErrorCodes[code]; ok { return true } // Go 的 crypto/tls 错误不用标准错误码,而是用消息字符串 lower := strings.ToLower(code) return strings.Contains(lower, "tls:") || strings.Contains(lower, "certificate") || strings.Contains(lower, "x509:") } // ============================================================ // classifyConnectionError - 连接错误分类 // ============================================================ // classifyConnectionError 将网络连接错误分类为 APIError. // // 精妙之处(CLEVER): 遍历 Go 的 error 链(errors.Unwrap)寻找具体的网络错误类型, // 类似早期方案的 cause chain walking(最深5层),但 Go 用 errors.As 更优雅. func classifyConnectionError(cause error) *APIError { apiErr := &APIError{ StatusCode: 0, Msg: cause.Error(), Cause: cause, Retry: &RetryInfo{Retryable: true}, // 连接错误默认可重试 } // 检查是否是 SSL/TLS 错误 if isSSLError(cause) { apiErr.ErrCategory = ErrSSL return apiErr } // 检查超时 if isTimeoutError(cause) { apiErr.ErrCategory = ErrTimeout return apiErr } // 通用连接错误 apiErr.ErrCategory = ErrConnection return apiErr } // isSSLError 检查错误链中是否包含 SSL/TLS 错误. func isSSLError(err error) bool { msg := err.Error() lower := strings.ToLower(msg) // Go crypto/tls 错误 if strings.Contains(lower, "tls:") || strings.Contains(lower, "x509:") { return true } // OpenSSL 错误码(通过 CGo 调用时可能出现) for code := range sslErrorCodes { if strings.Contains(msg, code) { return true } } return false } // isTimeoutError 检查错误链中是否包含超时错误. func isTimeoutError(err error) bool { // Go 标准方式:检查 net.Error 接口的 Timeout() 方法 var netErr net.Error if errors.As(err, &netErr) { return netErr.Timeout() } // 回退:字符串匹配 lower := strings.ToLower(err.Error()) return strings.Contains(lower, "timeout") || strings.Contains(lower, "deadline exceeded") }