Files
oci-portal/internal/oci/errors.go
T

179 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package oci
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"regexp"
"strings"
"github.com/oracle/oci-go-sdk/v65/common"
)
// ociErrorHints 把常见 OCI 错误码翻译成给用户看的中文提示。
var ociErrorHints = map[string]string{
"IncorrectState": "资源当前状态不允许该操作,通常是仍被其他资源占用或正在变更中",
"NotAuthorizedOrNotFound": "无权限或资源不存在(可能已被删除)",
"LimitExceeded": "已达到服务配额上限",
"QuotaExceeded": "compartment 配额不足",
"OutOfHostCapacity": "该可用域容量不足,可稍后重试或改用抢机任务",
"TooManyRequests": "请求过于频繁,请稍后再试",
"NotAuthenticated": "OCI 拒绝了请求签名:密钥无效或尚未生效。若刚替换过 API Key,全球生效需数分钟,请稍后重试",
"InvalidParameter": "请求参数无效",
"InternalError": "OCI 服务内部错误,请稍后重试",
"Conflict": "资源正在变更中,请稍后重试",
}
// ocidRe 匹配消息中的完整 OCID(unique 段 20 位以上才压缩,避免误伤短标识)。
var ocidRe = regexp.MustCompile(`ocid1\.([a-z0-9]+)\.[a-z0-9]*\.[a-z0-9-]*\.{1,2}[a-z0-9]{20,}`)
// proxyUserinfoRe 只匹配网络错误文本里位于 token / URL 起点的 userinfo@。
var proxyUserinfoRe = regexp.MustCompile(`(^|[\s/("'=])[^@\s/]+@`)
// shortenOcids 把消息里的长 OCID 压缩为「ocid1.<类型>…<尾6位>」,
// 保留资源类型与可比对的尾部,避免整条错误被 OCID 撑爆。
func shortenOcids(s string) string {
return ocidRe.ReplaceAllStringFunc(s, func(m string) string {
i := strings.Index(m, ".")
typ := m[i+1:]
if j := strings.Index(typ, "."); j > 0 {
typ = typ[:j]
}
return "ocid1." + typ + "…" + m[len(m)-6:]
})
}
// CompactError 把含 OCI ServiceError 的错误链提炼成短消息:
// 操作前缀 + 服务端消息,剥掉 SDK 附加的错误码、排障链接、
// 时间戳、SDK 版本等长文(错误码另经 API 的 ociCode 字段透出);
// 消息中的长 OCID 压缩为类型加尾位的短形式。非 OCI 错误原样返回。
func CompactError(err error) string {
var svcErr common.ServiceError
if !errors.As(err, &svcErr) {
return shortenOcids(err.Error())
}
prefix := err.Error()
if i := strings.Index(prefix, ": Error returned by"); i > 0 {
prefix = prefix[:i] + ": "
} else {
prefix = ""
}
return shortenOcids(prefix + svcErr.GetMessage())
}
// ErrorHint 返回常见 OCI 错误的中文提示;未识别返回空串。
// 容量不足常以 code=InternalError 返回,按消息内容归到容量提示。
func ErrorHint(err error) string {
var svcErr common.ServiceError
if !errors.As(err, &svcErr) {
return ""
}
if strings.Contains(svcErr.GetMessage(), "Out of host capacity") {
return ociErrorHints["OutOfHostCapacity"]
}
return ociErrorHints[svcErr.GetCode()]
}
// UpstreamNetworkError 判定错误链是否为上游连接层失败(代理拨号 / SOCKS 握手 /
// 超时 / DNS 等),命中返回给用户看的精简描述。已拿到 OCI 响应的 ServiceError
// 不在此列;判定保守:宁可漏归 500,不把业务错误误标为网络问题。
func UpstreamNetworkError(err error) (string, bool) {
var svcErr common.ServiceError
if errors.As(err, &svcErr) {
return "", false
}
var uerr *url.Error
if errors.As(err, &uerr) {
return classifyNetDetail(redactURLError(uerr)), true
}
if errors.Is(err, context.DeadlineExceeded) {
return "上游连接失败: 请求超时", true
}
var nerr net.Error
if errors.As(err, &nerr) && nerr.Timeout() {
return "上游连接失败: 连接超时", true
}
return "", false
}
// redactURLError 把 url.Error 的完整 URL 压缩为主机,并递归处理内层 url.Error。
func redactURLError(uerr *url.Error) string {
host := "<redacted-url>"
if u, err := url.Parse(uerr.URL); err == nil && u.Host != "" {
host = u.Host
}
return fmt.Sprintf("%s %s: %s", uerr.Op, host, redactURLCause(uerr.Err))
}
// redactURLCause 保留普通包装前缀,但用脱敏文本替换其中嵌套的 url.Error。
func redactURLCause(err error) string {
if err == nil {
return "unknown network error"
}
var nested *url.Error
if !errors.As(err, &nested) {
return redactProxyUserinfo(err.Error())
}
raw, nestedRaw := err.Error(), nested.Error()
if strings.Contains(raw, nestedRaw) {
raw = strings.Replace(raw, nestedRaw, redactURLError(nested), 1)
return redactProxyUserinfo(raw)
}
return redactURLError(nested)
}
// redactProxyUserinfo 清除存量非法代理 host 可能写入普通错误文本的 userinfo。
func redactProxyUserinfo(detail string) string {
return proxyUserinfoRe.ReplaceAllString(detail, "$1")
}
// classifyNetDetail 按细节特征加中文分类前缀:代理链路失败与一般上游失败分开表述。
func classifyNetDetail(detail string) string {
if strings.Contains(detail, "proxyconnect") || strings.Contains(detail, "socks connect") {
return "代理连接失败: " + detail
}
return "上游连接失败: " + detail
}
// ServiceStatus 返回错误链中 OCI ServiceError 的 HTTP 状态码;非服务端错误 ok=false。
func ServiceStatus(err error) (int, bool) {
var svcErr common.ServiceError
if errors.As(err, &svcErr) {
return svcErr.GetHTTPStatusCode(), true
}
return 0, false
}
// IsOnDemandUnsupported 识别「模型在该区域仅为微调基座、不支持按需调用」的 400:
// OCI 消息形如 "Not allowed to call finetune base model …, use Endpoint: false"。
// ListModels 无字段可事先区分,只能在调用报错时识别并换渠道。
func IsOnDemandUnsupported(err error) bool {
var svcErr common.ServiceError
if !errors.As(err, &svcErr) {
return false
}
return svcErr.GetHTTPStatusCode() == 400 &&
strings.Contains(strings.ToLower(svcErr.GetMessage()), "finetune base model")
}
// IsEntityNotFound 识别「实体不存在」404(消息 "Entity with key … not found"):
// GenAI 对区域内无按需供给的模型 OCID 返回此类 404,属模型级错误;
// 鉴权失败的 404 是 NotAuthorizedOrNotFound(消息为 Authorization failed…),不在此列。
func IsEntityNotFound(err error) bool {
var svcErr common.ServiceError
if !errors.As(err, &svcErr) {
return false
}
msg := strings.ToLower(svcErr.GetMessage())
return svcErr.GetHTTPStatusCode() == 404 &&
strings.Contains(msg, "entity with key") && strings.Contains(msg, "not found")
}
// IsModelUnavailable 归并「模型×区域不可按需调用」两类报错:微调基座 400 与实体不存在 404;
// 命中即应把该 (渠道, 模型) 从池中剔除并换候选/换渠道,而非定论租户配额问题。
func IsModelUnavailable(err error) bool {
return IsOnDemandUnsupported(err) || IsEntityNotFound(err)
}