fix(trace): 令牌守卫上移进 trace.UpstreamError,构造即安全
M48 复审的中危 finding:error.type 里的散文经 trace.UpstreamError 原样出镜,
因为守卫加在调用方(M46 的 model/voice 两处),构造函数自己不套守卫 ——
任何新调用点默认不安全,httpclient / vision / embedding / weixin 四条出口一直漏。
- trace.UpstreamError 构造时对 error.code / error.type 各走一次 ErrorToken
([A-Za-z0-9_.-]{<=64},不合规即空串):默认安全,不是可选加固。
- 构造后注入这条窄路收进 UpstreamErrorInfo.SetCode / SetType(同一道守卫),
供 DashScope 顶层 code 与 Anthropic 流内 error.type 两处使用 —— 调用方不再
各抄一份 ErrorToken。
- 删除因此冗余的调用方守卫:model.providerUpstreamError 与
voice.dashscopeOKBodyError。保留 voice 的 request_id 守卫(它不是 code/type,
构造函数看不见)。
- 四条漏的出口一行没改,只加用例证明守卫上移后自动安全;摘掉构造函数守卫时
四条用例 + model/voice 旧用例同时变红。
This commit is contained in:
1 parent
4e9cd17fbd
commit
fe3872ee28
10 files changed
+419
-17
No files matched your search
@@ -0,0 +1,57 @@
|
||||
package vision
|
||||
|
||||
// 本文件是 M49 的**出口回归探针**:令牌守卫上移进 trace.UpstreamError 之后,
|
||||
// 本适配器一行没改也应当自动安全 —— 视觉上游把散文塞进 error.code / error.type
|
||||
// 时,错误文本与日志里都搜不到它,而 status / body_bytes 这些诊断面照旧。
|
||||
//
|
||||
// 断言形状是「摘掉 trace.UpstreamError 里的守卫就变红」:这里没有任何本地守卫。
|
||||
// 视觉这一处尤其不能漏 —— 送上游的图像可能是她的照片,回显它的散文同样不能进日志。
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOpenAICompatibleAdapter_DropsFreeTextUpstreamCode 4xx 报文的 error.code /
|
||||
// error.type 里带散文 ⇒ 两个字段整体消失,status 与体积摘要仍在。
|
||||
func TestOpenAICompatibleAdapter_DropsFreeTextUpstreamCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
const prose = "Invalid value: '她刚才说的那句话'"
|
||||
body := `{"error":{"code":"` + prose + `","type":"` + prose + `"}}`
|
||||
|
||||
ad := newAdapter("https://vision.example.com")
|
||||
ad.client = &http.Client{Transport: stubTransport(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}, nil
|
||||
})}
|
||||
|
||||
_, err := ad.Describe(context.Background(), ImageRef{Source: SourceBase64, Data: "aGVsbG8="}, "")
|
||||
if err == nil {
|
||||
t.Fatal("400 却报成功")
|
||||
}
|
||||
if !errors.Is(err, ErrUpstream) {
|
||||
t.Fatalf("err = %v, want ErrUpstream", err)
|
||||
}
|
||||
|
||||
var logBuf bytes.Buffer
|
||||
slog.New(slog.NewJSONHandler(&logBuf, nil)).Error("vision call failed", "err", err)
|
||||
for _, surface := range []string{err.Error(), logBuf.String()} {
|
||||
if strings.Contains(surface, "她刚才说的那句话") {
|
||||
t.Fatalf("上游 error.code/type 里的散文进了错误或日志:%s", surface)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"status=400", "body_bytes="} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("错误里缺少 %q:%v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,13 @@ func classifyDashscopeStatus(endpoint string, status int, contentType string, bo
|
||||
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
|
||||
return fmt.Errorf("voice: %w", sentinel)
|
||||
}
|
||||
info.Code = trace.ErrorToken(fields.code)
|
||||
// 🔴 DashScope 把错误码放在**顶层** code 上(不是 error.code),构造函数看不见它,
|
||||
// 因此这里属于「构造后注入」:走 SetCode(同一道令牌守卫)。这不是重复守卫 ——
|
||||
// 去掉了它,散文形态的顶层 code 就会直接出镜(TestDashscopeTTS_Non200DropsFreeTextCode
|
||||
// 会变红)。
|
||||
info.SetCode(fields.code)
|
||||
// request_id 不进 UpstreamErrorInfo,也不由构造函数解析:它是被**拼进错误文本**的
|
||||
// 第三个自由文本面,所以它的守卫只能留在这里(这不是「同一件事守两遍」)。
|
||||
if rid := trace.ErrorToken(fields.requestID); rid != "" {
|
||||
return fmt.Errorf("voice: %w: request_id=%s: %w", sentinel, rid, info)
|
||||
}
|
||||
@@ -98,15 +104,14 @@ func decodeDashscopeErrorFields(body []byte) dashscopeErrorFields {
|
||||
// message 原文一律不进 error:它是上游自由文本,而这条请求的 input.url 是参考
|
||||
// 音频的公开地址(可能是预签名 URL,带凭据),上游报错经常把它整段回显出来。
|
||||
// 病因分类保留在 ErrProviderFailed(再试也不会好,与既有的熔断口径一致);
|
||||
// 确实带回来的 code/type 也只以令牌形态出镜(过 trace.ErrorToken)。
|
||||
// 确实带回来的 code/type 由 trace.UpstreamError **构造时**过令牌守卫(M49 上移),
|
||||
// 这里不再守第二遍。
|
||||
func dashscopeOKBodyError(endpoint string, body []byte) error {
|
||||
info, ok := trace.UpstreamError(trace.HostOf(endpoint), http.StatusOK, "", body).(*trace.UpstreamErrorInfo)
|
||||
if !ok {
|
||||
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
|
||||
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response", ErrProviderFailed)
|
||||
}
|
||||
info.Code = trace.ErrorToken(info.Code)
|
||||
info.Type = trace.ErrorToken(info.Type)
|
||||
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response: %w",
|
||||
ErrProviderFailed, info)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package voice
|
||||
|
||||
// 本文件是 M49 的**出口回归探针**:DashScope 的「HTTP 200 但业务失败」收口里,
|
||||
// error.code / error.type 的守卫已经上移进 trace.UpstreamError —— 本包那处调用点的
|
||||
// 重复守卫已删除,因此这里的断言直接钉在**构造函数**上(摘掉构造函数的守卫就变红)。
|
||||
//
|
||||
// 与它相邻的一条(TestDashscopeTTS_Non200DropsFreeTextCode)钉的是另一件事:
|
||||
// DashScope 的 4xx 把错误码放在**顶层** code(构造函数看不见),那一处靠
|
||||
// UpstreamErrorInfo.SetCode 注入,守卫仍在。两条合起来覆盖这一族的两个面。
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDashscopeOKBodyError_DropsFreeTextErrorCode 200 报文的 error.code / error.type
|
||||
// 里带散文(TTS 场景下可能是她要念的台词被回显)⇒ 两个字段整体消失,而 status=200
|
||||
// 与 body_bytes(体积摘要)仍在。
|
||||
func TestDashscopeOKBodyError_DropsFreeTextErrorCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
const prose = "Invalid value: '她刚才说的那句话'"
|
||||
body := []byte(`{"error":{"code":"` + prose + `","type":"` + prose + `"}}`)
|
||||
|
||||
err := dashscopeOKBodyError("https://dashscope.example.com/api/v1/services/audio/tts", body)
|
||||
if !errors.Is(err, ErrProviderFailed) {
|
||||
t.Fatalf("err = %v, want ErrProviderFailed(熔断口径不变)", err)
|
||||
}
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "她刚才说的那句话") {
|
||||
t.Fatalf("上游 error.code/type 里的散文进了错误:%v", msg)
|
||||
}
|
||||
if strings.Contains(msg, " code=") || strings.Contains(msg, " type=") {
|
||||
t.Fatalf("非令牌形状的 code/type 没有被丢弃:%v", msg)
|
||||
}
|
||||
for _, want := range []string{"status=200", "body_bytes="} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Fatalf("错误里缺少 %q(诊断面不能一起丢):%v", want, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user