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 旧用例同时变红。
57 lines
2.2 KiB
Go
57 lines
2.2 KiB
Go
package httpclient
|
||
|
||
// 本文件是 M49 的**出口回归探针**:令牌守卫上移进 trace.UpstreamError 之后,
|
||
// 本客户端一行没改也应当自动安全 —— 上游把散文塞进 error.code / error.type 时,
|
||
// 错误文本与日志里都搜不到它,而 status / body_bytes 这些诊断面照旧。
|
||
//
|
||
// 断言形状是「摘掉 trace.UpstreamError 里的守卫就变红」:这里**没有任何本地守卫**,
|
||
// 也就没有任何本地补丁能掩盖缺口。M46 的教训正是「守卫加在调用方 ⇒ 新调用点默认
|
||
// 不安全」,所以这一条故意不去调用点补。
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"log/slog"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestDoJSON_StatusErrorDropsFreeTextUpstreamCode 4xx 报文的 error.code / error.type
|
||
// 里带散文(带空格与标点,不像协议令牌)⇒ 两个字段整体消失,而状态码与体积摘要仍在。
|
||
func TestDoJSON_StatusErrorDropsFreeTextUpstreamCode(t *testing.T) {
|
||
t.Parallel()
|
||
const prose = "Invalid value: '她刚才说的那句话'"
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadGateway)
|
||
_, _ = w.Write([]byte(`{"error":{"code":"` + prose + `","type":"` + prose + `"}}`))
|
||
}))
|
||
defer srv.Close()
|
||
|
||
c := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client()), WithLogger(discardLogger()))
|
||
_, err := c.DoJSON(context.Background(), http.MethodPost, "/message/create", map[string]any{"a": 1})
|
||
if err == nil {
|
||
t.Fatal("502 却报成功")
|
||
}
|
||
if !errors.Is(err, ErrAPI) {
|
||
t.Fatalf("err = %v, want ErrAPI", err)
|
||
}
|
||
|
||
var logBuf bytes.Buffer
|
||
slog.New(slog.NewJSONHandler(&logBuf, nil)).Error("upstream 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=502", "body_bytes="} {
|
||
if !strings.Contains(err.Error(), want) {
|
||
t.Fatalf("错误里缺少 %q:%v", want, err)
|
||
}
|
||
}
|
||
}
|