model / multimodal / tts / embedding 里所有「把 net/http 的 *url.Error 直接打进 错误串」的地方(M51 清单的 B1 组 17 处 + 清单外的同根因 2 处)统一改走 trace.URLCause:错误串只少一段地址,底层原因、哨兵分类、状态码、重试/降级判据 一字不动。 清单内(17): - model/openai.go: Chat / ChatStream 的 NewRequest、doRequest - model/anthropic.go: chatOnce / streamOnce 的 NewRequest、doRequest - multimodal/vision/adapter.go: ask 的 NewRequest、client.Do - multimodal/voice/cloud.go: postJSON 的 NewRequest、client.Do - multimodal/voice/dashscope.go: download(预签名 OSS 地址)与 dashscopePost 各两处 - voice/tts/client.go: unreachable(返回值与同一行日志的 err 字段都只留原因, 地址只由 redactURL(endpoint) 出现一次) - embedding/api.go: embedChunk 的 client.Do、newEmbeddingRequest 的 NewRequest 清单外同根因(2,均为 %w 形态,B1 清单按 %v 搜时漏掉): - voice/tts/client.go: speak 的 NewRequest 失败 - embedding/endpoint.go: normalizeEndpointURL 的 url.Parse 失败 测试:5 个新文件 18 个用例,逐处可变异打红(实测:把任一处的 URLCause 去掉 ⇒ 对应用例红);dashscope 预签名地址单独一条用例,并另有一条钉住 403 状态码仍在。
81 lines
3.0 KiB
Go
81 lines
3.0 KiB
Go
package vision
|
|
|
|
// 本文件是 M52 的出口回归探针:vision 适配器里「把 net/http 的 *url.Error 直接打进
|
|
// 错误串」的两处(建请求 / 发送)一律先过 trace.URLCause。
|
|
//
|
|
// 病害形状:`client.Do` 失败时 net/http 包出
|
|
// `Post "https://user:***@h/p?access_token=…": dial tcp …` —— 密码被打码,但用户名与
|
|
// 整条 query(含凭据/签名)原样在串里;`http.NewRequest` 的解析失败则把整条 base URL
|
|
// 带出来。这条串会进日志与工单。
|
|
//
|
|
// 断言形状是「把任一处改回 err ⇒ 对应用例变红」:两处各一个用例,且都同时钉住
|
|
// 「只少地址、不少原因」(底层原因与 ErrUpstream 哨兵必须还在)。
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
urlCauseQuerySecret = "SECRET-2f8a1c"
|
|
urlCauseUser = "svc-user"
|
|
urlCauseBadHost = "bad host.example.com"
|
|
)
|
|
|
|
func visionURLCauseGoodBase() string {
|
|
return "https://" + urlCauseUser + ":svc-pass@vision.example.com/v1?access_token=" + urlCauseQuerySecret
|
|
}
|
|
|
|
func visionURLCauseBadBase() string {
|
|
return "https://" + urlCauseUser + ":svc-pass@" + urlCauseBadHost + "/v1?access_token=" + urlCauseQuerySecret
|
|
}
|
|
|
|
// TestDescribeDropsURLFromTransportError 钉住 ask 里 `a.client.Do` 失败那一处。
|
|
func TestDescribeDropsURLFromTransportError(t *testing.T) {
|
|
t.Parallel()
|
|
ad := newAdapter(visionURLCauseGoodBase())
|
|
ad.client = &http.Client{Transport: stubTransport(func(*http.Request) (*http.Response, error) {
|
|
return nil, errors.New("dial tcp 10.0.0.1:443: connect: connection refused")
|
|
})}
|
|
|
|
_, err := ad.Describe(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}, "")
|
|
if err == nil {
|
|
t.Fatal("want error, got nil")
|
|
}
|
|
if strings.Contains(err.Error(), urlCauseQuerySecret) {
|
|
t.Fatalf("错误串带出了 query 里的凭据:%v", err)
|
|
}
|
|
if strings.Contains(err.Error(), urlCauseUser) {
|
|
t.Fatalf("错误串带出了 userinfo 里的用户名:%v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "connection refused") {
|
|
t.Fatalf("错误串丢了底层原因(只许少地址):%v", err)
|
|
}
|
|
if !errors.Is(err, ErrUpstream) {
|
|
t.Fatalf("err = %v, want ErrUpstream(分类不许被带偏)", err)
|
|
}
|
|
}
|
|
|
|
// TestDescribeDropsURLFromBuildRequestError 钉住 ask 里 `http.NewRequest` 失败那一处。
|
|
func TestDescribeDropsURLFromBuildRequestError(t *testing.T) {
|
|
t.Parallel()
|
|
ad := newAdapter(visionURLCauseBadBase())
|
|
|
|
_, err := ad.Describe(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}, "")
|
|
if err == nil {
|
|
t.Fatal("want error, got nil")
|
|
}
|
|
if strings.Contains(err.Error(), urlCauseQuerySecret) || strings.Contains(err.Error(), urlCauseUser) {
|
|
t.Fatalf("建请求失败的串带出了 base URL 里的凭据:%v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid character") {
|
|
t.Fatalf("错误串丢了底层解析原因(只许少地址):%v", err)
|
|
}
|
|
if !errors.Is(err, ErrUpstream) {
|
|
t.Fatalf("err = %v, want ErrUpstream(分类不许被带偏)", err)
|
|
}
|
|
}
|