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 承担分类。 每条新增/改写用例都做过变异验证(把字段拼回去即变红)。
This commit is contained in:
1 parent
3a0cc64160
commit
996201c4ed
11 files changed
+432
-127
No files matched your search
@@ -54,8 +54,6 @@ const (
|
||||
// uploadMaxAttempts 是 CDN 上传的最大尝试次数(官方 cdn-upload.ts:7 的
|
||||
// UPLOAD_MAX_RETRIES=3,无退避重试)。
|
||||
uploadMaxAttempts = 3
|
||||
// uploadErrMaxChars 是进日志/错误的服务端错误说明的字符上限。
|
||||
uploadErrMaxChars = 200
|
||||
|
||||
// encryptTypePacked 是 CDNMedia.encrypt_type 的取值:1 = 打包了缩略图/中图等信息
|
||||
// (官方 src/api/types.ts:95 的注释)。官方出站三种媒体都固定填 1
|
||||
@@ -199,7 +197,8 @@ func (a *Adapter) uploadURLFromParam(param, fileKey string) string {
|
||||
// - `src/cdn/cdn-upload.ts:61`:下载凭据在**响应头 `x-encrypted-param`** 里,拿到后
|
||||
// 填进 item 的 `media.encrypt_query_param`(`src/cdn/upload.ts:17` 的注释写明);
|
||||
// 响应体本身不是凭据来源 —— 这正是旧实现「PUT 上去就当成功」丢掉的那一环;
|
||||
// - `src/cdn/cdn-upload.ts:40-83`:4xx 立即放弃(附服务端 `x-error-message`),
|
||||
// - `src/cdn/cdn-upload.ts:40-83`:4xx 立即放弃(官方附服务端 `x-error-message`;
|
||||
// 本项目**不把它拼进错误** —— 上游自由文本面一律默认不带,见 uploadOnce),
|
||||
// 其余失败最多重试 3 次(无退避);
|
||||
// - `src/cdn/cdn-upload.ts:62-65`:200 但没有该响应头同样是失败(不得当成成功)。
|
||||
//
|
||||
@@ -243,10 +242,12 @@ func (a *Adapter) uploadOnce(ctx context.Context, uploadURL string, data []byte)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||
// 服务端把原因放在 x-error-message 头里(官方 cdn-upload.ts:48);它是对我们
|
||||
// 这次请求的说明,不是凭据,可以进错误与日志(截断,避免服务端回显灌爆日志)。
|
||||
return "", false, fmt.Errorf("weixin_ilink: upload media: status %d: %s",
|
||||
resp.StatusCode, truncateForLog(resp.Header.Get("x-error-message"), uploadErrMaxChars))
|
||||
// 服务端把原因放在 x-error-message 头里(官方 cdn-upload.ts:48)。它是**上游自由
|
||||
// 文本面**(与信封 message / Bot API description / OneBot msg 同族),因此
|
||||
// **默认不进**错误与日志(M50 口径):上游可以在里面回显我们上传的东西,而同一道
|
||||
// 边界在别处才刚收紧过。status 已经足以分类(4xx = 请求本身不对,立即放弃),
|
||||
// 所以这里只留 status。
|
||||
return "", false, fmt.Errorf("weixin_ilink: upload media: status %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", true, fmt.Errorf("weixin_ilink: upload media: status %d", resp.StatusCode)
|
||||
@@ -316,17 +317,6 @@ func encodeMediaAesKey(key []byte) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(key)))
|
||||
}
|
||||
|
||||
// truncateForLog 按 rune 截断一段外来文本,用于把服务端的错误说明放进日志/错误而不
|
||||
// 让它撑爆日志行。它只被喂给「服务端对我们这次请求的说明」,不是凭据。
|
||||
func truncateForLog(s string, max int) string {
|
||||
trimmed := strings.TrimSpace(s)
|
||||
runes := []rune(trimmed)
|
||||
if len(runes) <= max {
|
||||
return trimmed
|
||||
}
|
||||
return string(runes[:max]) + "…"
|
||||
}
|
||||
|
||||
// uploadMediaTypeFor 把归一化的 MediaKind 映射到 getuploadurl 的 media_type 码。
|
||||
func uploadMediaTypeFor(kind adapter.MediaKind) int {
|
||||
switch kind {
|
||||
|
||||
@@ -264,14 +264,16 @@ func TestOutboundMedia_UploadWithoutParamHeaderIsHonest(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestOutboundMedia_UploadClientErrorIsNotRetried 覆盖 ④-b:4xx 是客户端错误,重试徒劳
|
||||
// (官方 cdn-upload.ts:47-52 立即抛出),且服务端的 `x-error-message` 必须进错误信息 ——
|
||||
// 失败要可诊断。
|
||||
// (官方 cdn-upload.ts:47-52 立即抛出)。且服务端的 `x-error-message` **不进错误**
|
||||
// (M50 口径:它是上游自由文本面,与信封 message / Bot API description / OneBot msg
|
||||
// 同族;4xx 这个 status 已足够分类)—— 变异:把它拼回错误 ⇒ 本用例后半段立刻红。
|
||||
func TestOutboundMedia_UploadClientErrorIsNotRetried(t *testing.T) {
|
||||
t.Parallel()
|
||||
const sentinel = "invalid content: 她刚才说的那句话"
|
||||
s := newUploadCaptureServer(t, handlerResult{
|
||||
status: http.StatusBadRequest,
|
||||
body: []byte("ignored"),
|
||||
headers: map[string]string{"x-error-message": "bad filekey"},
|
||||
headers: map[string]string{"x-error-message": sentinel},
|
||||
})
|
||||
ad := newConnectedAdapter(t, Config{BaseURL: s.srv.URL, CDNBaseURL: s.srv.URL, BotToken: "tok"}, s.srv)
|
||||
ad.recordInbound("tok-1", adapter.UnifiedMessage{SenderID: "u-1", Timestamp: ad.clock()})
|
||||
@@ -285,8 +287,11 @@ func TestOutboundMedia_UploadClientErrorIsNotRetried(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("4xx 上传失败却报成功")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "bad filekey") {
|
||||
t.Fatalf("错误里没有服务端说明:%v", err)
|
||||
if strings.Contains(err.Error(), "她刚才说的那句话") {
|
||||
t.Fatalf("服务端的 x-error-message 进了错误:%v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "status 400") {
|
||||
t.Fatalf("剥掉服务端说明时把 status 一起丢了:%v", err)
|
||||
}
|
||||
if s.attempts != 1 {
|
||||
t.Fatalf("4xx 被重试了:上传尝试 %d 次, want 1", s.attempts)
|
||||
|
||||
Reference in new issue
Block a user