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) } } }