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:
AskaEth committed 2026-10-26 19:13:38 +08:00
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)
}
}
}