滑动续期、网络错误归类、API Key 激活乐观锁与生效提示
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -16,6 +20,7 @@ var ociErrorHints = map[string]string{
|
||||
"QuotaExceeded": "compartment 配额不足",
|
||||
"OutOfHostCapacity": "该可用域容量不足,可稍后重试或改用抢机任务",
|
||||
"TooManyRequests": "请求过于频繁,请稍后再试",
|
||||
"NotAuthenticated": "OCI 拒绝了请求签名:密钥无效或尚未生效。若刚替换过 API Key,全球生效需数分钟,请稍后重试",
|
||||
"InvalidParameter": "请求参数无效",
|
||||
"InternalError": "OCI 服务内部错误,请稍后重试",
|
||||
"Conflict": "资源正在变更中,请稍后重试",
|
||||
@@ -24,6 +29,9 @@ var ociErrorHints = map[string]string{
|
||||
// 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 {
|
||||
@@ -68,6 +76,67 @@ func ErrorHint(err error) string {
|
||||
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
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -95,6 +97,11 @@ func TestErrorHint(t *testing.T) {
|
||||
err: fmt.Errorf("launch: %w", fakeServiceError{500, "InternalError", "Out of host capacity."}),
|
||||
want: ociErrorHints["OutOfHostCapacity"],
|
||||
},
|
||||
{
|
||||
name: "NotAuthenticated 提示密钥无效或传播中",
|
||||
err: fmt.Errorf("get instance: %w", fakeServiceError{401, "NotAuthenticated", "The required information ..."}),
|
||||
want: ociErrorHints["NotAuthenticated"],
|
||||
},
|
||||
{
|
||||
name: "未知错误码返回空串",
|
||||
err: fmt.Errorf("x: %w", fakeServiceError{400, "SomethingNew", "boom"}),
|
||||
@@ -158,3 +165,106 @@ func TestIsModelUnavailable(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeTimeoutErr 模拟实现 net.Error 的超时错误(如响应体读取超时)。
|
||||
type fakeTimeoutErr struct{}
|
||||
|
||||
func (fakeTimeoutErr) Error() string { return "read tcp 10.0.0.1:443: i/o timeout" }
|
||||
func (fakeTimeoutErr) Timeout() bool { return true }
|
||||
func (fakeTimeoutErr) Temporary() bool { return false }
|
||||
|
||||
type upstreamNetworkErrorCase struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
wantOK bool
|
||||
}
|
||||
|
||||
var upstreamNetworkErrorCases = []upstreamNetworkErrorCase{
|
||||
{
|
||||
name: "OCI 服务错误不归网络",
|
||||
err: fmt.Errorf("get instance: %w", fakeServiceError{status: 401, code: "NotAuthenticated", message: "x"}),
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "普通业务错误不归网络",
|
||||
err: errors.New("parse payload: bad json"),
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "代理 CONNECT 失败带中文分类且 URL 缩为主机",
|
||||
err: fmt.Errorf("list instances: %w", &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://iaas.uk-london-1.oraclecloud.com/20160918/instances?limit=100",
|
||||
Err: errors.New("proxyconnect tcp: dial tcp 1.2.3.4:8080: connect: connection refused"),
|
||||
}),
|
||||
want: "代理连接失败: Get iaas.uk-london-1.oraclecloud.com: proxyconnect tcp: dial tcp 1.2.3.4:8080: connect: connection refused",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "SOCKS 握手失败归代理类",
|
||||
err: &url.Error{
|
||||
Op: "Post",
|
||||
URL: "https://identity.us-sanjose-1.oci.oraclecloud.com/20160918/users",
|
||||
Err: errors.New("socks connect tcp 5.6.7.8:1080->identity: dial refused"),
|
||||
},
|
||||
want: "代理连接失败: Post identity.us-sanjose-1.oci.oraclecloud.com: socks connect tcp 5.6.7.8:1080->identity: dial refused",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "URL 内嵌 userinfo 不回显",
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://user:pass@example.com/path",
|
||||
Err: errors.New("EOF"),
|
||||
},
|
||||
want: "上游连接失败: Get example.com: EOF",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "嵌套 URL 的代理 userinfo 不回显",
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://identity.us-ashburn-1.oraclecloud.com/20160918/tenancies/x",
|
||||
Err: fmt.Errorf("proxyconnect tcp: %w", &url.Error{
|
||||
Op: "Dial", URL: "http://proxy-user:proxy-pass@proxy.example.com:8080",
|
||||
Err: errors.New("connect: connection refused"),
|
||||
}),
|
||||
},
|
||||
want: "代理连接失败: Get identity.us-ashburn-1.oraclecloud.com: proxyconnect tcp: Dial proxy.example.com:8080: connect: connection refused",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "普通内层错误中的存量代理 userinfo 不回显",
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://identity.us-phoenix-1.oraclecloud.com/20160918/tenancies/x",
|
||||
Err: errors.New("proxyconnect tcp: dial tcp alice:secret@proxy.example.com:8080: connect: connection refused"),
|
||||
},
|
||||
want: "代理连接失败: Get identity.us-phoenix-1.oraclecloud.com: proxyconnect tcp: dial tcp proxy.example.com:8080: connect: connection refused",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "context 超时归网络",
|
||||
err: fmt.Errorf("summarize costs: %w", context.DeadlineExceeded),
|
||||
want: "上游连接失败: 请求超时",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "net.Error 超时归网络",
|
||||
err: fmt.Errorf("read body: %w", fakeTimeoutErr{}),
|
||||
want: "上游连接失败: 连接超时",
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestUpstreamNetworkError(t *testing.T) {
|
||||
for _, tt := range upstreamNetworkErrorCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := UpstreamNetworkError(tt.err)
|
||||
if ok != tt.wantOK || got != tt.want {
|
||||
t.Errorf("UpstreamNetworkError() = (%q, %v), want (%q, %v)", got, ok, tt.want, tt.wantOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user