// 连接诊断系统 -- SSL 错误码目录 + 用户可操作提示. // // 精妙之处(CLEVER): 企业用户背后往往有 TLS 拦截代理(Zscaler,Palo Alto 等), // 拿到 "UNABLE_TO_VERIFY_LEAF_SIGNATURE" 这种 OpenSSL 错误码根本不知道怎么办. // 把 19 个高频 SSL 错误码映射为可操作的诊断提示,一条消息省一轮技术支持. // // 升华改进(ELEVATED): 不只 SSL--扩展为通用的连接诊断, // 涵盖 DNS,超时,连接拒绝等所有网络层错误. // 仓储场景同样需要:PLC 连接超时,MQTT broker 不可达,OPC-UA 证书过期. // // 替代方案:<原方案只处理 SSL,其他连接错误返回 "Connection error"> package api import ( "errors" "net" "strings" ) // ============================================================ // DiagnosticHinter - 诊断提示接口 // ============================================================ // DiagnosticHinter 为 API 错误提供用户可操作的诊断提示. // // 升华改进(ELEVATED): 接口化而非硬编码--不同部署环境可以注册不同的 Hinter. // 例如企业内网环境注册一个知道内部代理地址的 Hinter, // 云环境注册一个检查安全组/防火墙的 Hinter. type DiagnosticHinter interface { // Hint 返回用户可操作的诊断提示,空字符串表示没有建议. Hint(err *APIError) string } // ============================================================ // 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:") } // ============================================================ // sslHints - SSL 错误码到诊断提示的映射 // ============================================================ // sslHints 将特定 SSL 错误码映射为用户可操作的提示. var sslHints = map[string]string{ "UNABLE_TO_VERIFY_LEAF_SIGNATURE": "SSL 证书验证失败。如果你在企业代理或 TLS 拦截防火墙后面,请设置 SSL_CERT_FILE 或联系 IT 部门", "UNABLE_TO_GET_ISSUER_CERT": "SSL 证书验证失败。如果你在企业代理或 TLS 拦截防火墙后面,请设置 SSL_CERT_FILE 或联系 IT 部门", "UNABLE_TO_GET_ISSUER_CERT_LOCALLY": "SSL 证书验证失败。如果你在企业代理或 TLS 拦截防火墙后面,请设置 SSL_CERT_FILE 或联系 IT 部门", "CERT_HAS_EXPIRED": "SSL 证书已过期", "CERT_REVOKED": "SSL 证书已被吊销", "DEPTH_ZERO_SELF_SIGNED_CERT": "检测到自签名证书。如果你在企业代理后面,请设置 SSL_CERT_FILE 或联系 IT 部门", "SELF_SIGNED_CERT_IN_CHAIN": "检测到自签名证书。如果你在企业代理后面,请设置 SSL_CERT_FILE 或联系 IT 部门", "ERR_TLS_CERT_ALTNAME_INVALID": "SSL 证书主机名不匹配", "HOSTNAME_MISMATCH": "SSL 证书主机名不匹配", "CERT_NOT_YET_VALID": "SSL 证书尚未生效(检查系统时钟)", "ERR_TLS_HANDSHAKE_TIMEOUT": "TLS 握手超时。检查网络连接和代理设置", } // ============================================================ // DefaultHinter - 默认诊断提示器 // ============================================================ // DefaultHinter 是默认的诊断提示器,覆盖 SSL 和常见网络错误. type DefaultHinter struct{} // Hint 返回诊断提示. func (h *DefaultHinter) Hint(err *APIError) string { if err == nil { return "" } switch err.ErrCategory { case ErrSSL: return h.sslHint(err) case ErrConnection: return h.connectionHint(err) case ErrTimeout: return "请求超时。检查网络连接和代理设置" default: return "" } } // sslHint 为 SSL 错误提供诊断提示. func (h *DefaultHinter) sslHint(err *APIError) string { if err.Cause == nil && err.Msg == "" { return "" } // 尝试从错误消息中匹配已知 SSL 错误码 msg := err.Msg if err.Cause != nil { msg = err.Cause.Error() } for code, hint := range sslHints { if strings.Contains(msg, code) { return hint } } // Go crypto/tls 错误 if strings.Contains(msg, "x509:") || strings.Contains(msg, "tls:") { return "SSL/TLS 证书错误。如果你在企业代理后面,请设置 SSL_CERT_FILE 环境变量指向 CA 证书包" } return "SSL/TLS 错误。检查证书配置和代理设置" } // connectionHint 为连接错误提供诊断提示. func (h *DefaultHinter) connectionHint(err *APIError) string { msg := err.Msg if err.Cause != nil { msg = err.Cause.Error() } lower := strings.ToLower(msg) if strings.Contains(lower, "no such host") || strings.Contains(lower, "dns") { return "DNS 解析失败。检查 API 地址和 DNS 配置" } if strings.Contains(lower, "connection refused") { return "连接被拒绝。检查 API 地址和端口是否正确" } if strings.Contains(lower, "connection reset") { return "连接被重置。可能是代理或防火墙阻断了连接" } if strings.Contains(lower, "no route to host") { return "无法到达主机。检查网络连接" } return "网络连接错误。检查网络连接和代理设置" } // ============================================================ // CompositeHinter - 叠加诊断提示器 // ============================================================ // CompositeHinter 组合多个 Hinter,第一个返回非空提示的胜出. type CompositeHinter struct { hinters []DiagnosticHinter } // NewCompositeHinter 创建组合提示器. func NewCompositeHinter(hinters ...DiagnosticHinter) *CompositeHinter { return &CompositeHinter{hinters: hinters} } // Add 添加诊断提示器. func (c *CompositeHinter) Add(hinter DiagnosticHinter) { c.hinters = append(c.hinters, hinter) } // Hint 按顺序调用提示器. func (c *CompositeHinter) Hint(err *APIError) string { for _, hinter := range c.hinters { if hint := hinter.Hint(err); hint != "" { return hint } } return "" } // ============================================================ // 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") }