Files
AskaEth 996201c4ed fix(httpclient): 信封 message 默认不进错误,渠道上游自由文本一并收口
信封路径(非 2xx + 信封体、2xx + code!=0)此前把上游 message 经 RedactText
拼进 error,而上游会在 message 里回显被拒的请求正文 —— 她的原话因此进了错误
与调用方日志(M49 探针实测:code 1: invalid content: 她刚才说的那句话 ...)。

- envelope message 改为**默认不带**(新 opt-in WithEnvelopeMessage(),与
  trace.UpstreamError 的 message 同一口径);code 与上游 status 保留,
  errors.Is 的 ErrAPI / trace.ErrUpstream 链不变。
- opt-in 下统一走单一清洗 upstreamText:压平空白 + trace.RedactTextLimited,
  上限 40 rune(比 trace 的 128 短:错误里那句话是提示,不是摘要)。
- plugins/channels 同族出口一并收口(扫过全部,命中 4 处):
  telegram description、qq_obv11 msg(错误 + logEchoFailure 日志)、
  weixin_ilink CDN x-error-message;qq 的 status 字段改走 trace.ErrorToken
  令牌守卫。被剥掉的一律由 status / error_code / retcode 承担分类。

每条新增/改写用例都做过变异验证(把字段拼回去即变红)。
2026-10-26 19:28:15 +08:00

132 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package qq_obv11
// 本文件钉住 OneBot 信封里**自由文本面**的出镜边界(M50,与 httpclient 的信封
// message、Telegram 的 description、iLink 的 x-error-message 同一族):
//
// - 失败时上游的 `msg` / `wording` **不进错误文本**:NapCat / go-cqhttp 会把失败的
// 请求内容回显在 msg 里,而那些内容里就有她要发的话。留下的是 retcode 与 status。
// - status 是**协议字段**,但同样只按令牌出镜(trace.ErrorToken):上游若在 status
// 里塞散文,那一段整体消失。
// - 回执失败的 WARN 日志(logEchoFailure)走同一道边界 —— 否则同一个洞只是换个出口。
//
// 每条都是「把字段拼回去就变红」的形状,断言的是**错误文本与日志行**。
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// newFailingOneBotAPI 起一个总是回「失败信封」的假 OneBot,body 由调用方给。
func newFailingOneBotAPI(t *testing.T, body map[string]any) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
t.Cleanup(srv.Close)
return srv
}
// TestHTTPClientDropsUpstreamMsg 失败信封里的 msg / wording 带她的原话 ⇒ 错误与日志里
// 都搜不到,而 retcode 与 status 仍在(变异的两个方向各钉一条)。
func TestHTTPClientDropsUpstreamMsg(t *testing.T) {
t.Parallel()
const sentinel = "她刚才说的那句话"
srv := newFailingOneBotAPI(t, map[string]any{
"status": "failed",
"retcode": 1404,
"msg": "SEND_MSG_API_ERROR: invalid content: " + sentinel,
"wording": "参数错误:" + sentinel,
"data": nil,
})
c, err := newOBHTTPClient(srv.URL, "tok")
if err != nil {
t.Fatalf("newOBHTTPClient: %v", err)
}
payload, err := buildActionReq("send_msg", map[string]any{"message": sentinel}, "")
if err != nil {
t.Fatalf("buildActionReq: %v", err)
}
_, err = c.post(context.Background(), payload)
if err == nil {
t.Fatal("retcode=1404 却报成功")
}
var logBuf bytes.Buffer
slog.New(slog.NewJSONHandler(&logBuf, nil)).Error("onebot action failed", "err", err)
for _, surface := range []string{err.Error(), logBuf.String()} {
if strings.Contains(surface, sentinel) {
t.Fatalf("上游 msg / wording 进了错误或日志:%s", surface)
}
}
if !strings.Contains(err.Error(), "retcode=1404") {
t.Fatalf("retcode 丢了(唯一可靠的分类信息):%v", err)
}
if !strings.Contains(err.Error(), `status="failed"`) {
t.Fatalf("status 丢了(它是协议字段,应当仍出镜):%v", err)
}
}
// TestHTTPClientDropsProseStatus 上游若把散文塞进 status,令牌守卫让它整体消失 ——
// status 不是第二条正文通道。
func TestHTTPClientDropsProseStatus(t *testing.T) {
t.Parallel()
srv := newFailingOneBotAPI(t, map[string]any{
"status": "failed: 她刚才说的那句话",
"retcode": 1404,
"data": nil,
})
c, err := newOBHTTPClient(srv.URL, "tok")
if err != nil {
t.Fatalf("newOBHTTPClient: %v", err)
}
payload, err := buildActionReq("send_msg", map[string]any{"message": "hi"}, "")
if err != nil {
t.Fatalf("buildActionReq: %v", err)
}
_, err = c.post(context.Background(), payload)
if err == nil {
t.Fatal("retcode=1404 却报成功")
}
if strings.Contains(err.Error(), "她刚才说的那句话") || strings.Contains(err.Error(), "status=") {
t.Fatalf("散文 status 出镜了(应被令牌守卫丢弃):%v", err)
}
if !strings.Contains(err.Error(), "retcode=1404") {
t.Fatalf("retcode 丢了:%v", err)
}
}
// TestLogEchoFailureDropsUpstreamMsg 回执失败的 WARN 是**另一个出口**:它同样只记
// retcode 与 status,不记上游 msg(同一个洞换个出口的形态)。
func TestLogEchoFailureDropsUpstreamMsg(t *testing.T) {
t.Parallel()
const sentinel = "她刚才说的那句话"
var buf bytes.Buffer
a := NewAdapter(Config{}, WithLogger(slog.New(slog.NewJSONHandler(&buf, nil))))
a.logEchoFailure(&obAPIResponse{
Status: "failed",
RetCode: 1404,
Msg: "SEND_MSG_API_ERROR: invalid content: " + sentinel,
Wording: "参数错误:" + sentinel,
})
line := buf.String()
if line == "" {
t.Fatal("回执失败一条日志都没有(静默即缺陷)")
}
if strings.Contains(line, sentinel) {
t.Fatalf("上游 msg 进了日志:%s", line)
}
if !strings.Contains(line, `"retcode":1404`) || !strings.Contains(line, `"status":"failed"`) {
t.Fatalf("日志里没有 retcode/status(诊断面):%s", line)
}
}