feat: 网关请求头 wire casing 保持、转发行为开关、调试日志增强及 accept-encoding 恢复
- 新增 header_util.go,通过 setHeaderRaw/getHeaderRaw/addHeaderRaw 绕过 Go 的 canonical-case 规范化,保持真实 Claude CLI 抓包的请求头大小写 (如 "x-app" 而非 "X-App","X-Stainless-OS" 而非 "X-Stainless-Os") - 新增管理后台开关:指纹统一化(默认开启)和 metadata 透传(默认关闭), 使用 atomic.Value + singleflight 缓存模式,60s TTL - 调试日志从控制台 body 打印升级为文件级完整快照 (按真实 wire 顺序输出 headers + 格式化 JSON body + 上下文元数据) - 恢复 accept-encoding 到白名单,在 http_upstream.go 新增 decompressResponseBody 处理 gzip/brotli/deflate 解压(Go 显式设置 Accept-Encoding 时不会自动解压) - OAuth 服务 axios UA 从 1.8.4 更新至 1.13.6 - 测试断言改用 getHeaderRaw 适配 raw header 存储方式
This commit is contained in:
@@ -212,7 +212,7 @@ func (s *claudeOAuthService) ExchangeCodeForToken(ctx context.Context, code, cod
|
||||
SetContext(ctx).
|
||||
SetHeader("Accept", "application/json, text/plain, */*").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("User-Agent", "axios/1.8.4").
|
||||
SetHeader("User-Agent", "axios/1.13.6").
|
||||
SetBody(reqBody).
|
||||
SetSuccessResult(&tokenResp).
|
||||
Post(s.tokenURL)
|
||||
@@ -250,7 +250,7 @@ func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, pro
|
||||
SetContext(ctx).
|
||||
SetHeader("Accept", "application/json, text/plain, */*").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("User-Agent", "axios/1.8.4").
|
||||
SetHeader("User-Agent", "axios/1.13.6").
|
||||
SetBody(reqBody).
|
||||
SetSuccessResult(&tokenResp).
|
||||
Post(s.tokenURL)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -13,6 +15,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyutil"
|
||||
@@ -143,6 +147,9 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果上游返回了压缩内容,解压后再交给业务层
|
||||
decompressResponseBody(resp)
|
||||
|
||||
// 包装响应体,在关闭时自动减少计数并更新时间戳
|
||||
// 这确保了流式响应(如 SSE)在完全读取前不会被淘汰
|
||||
resp.Body = wrapTrackedBody(resp.Body, func() {
|
||||
@@ -218,6 +225,9 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco
|
||||
|
||||
slog.Debug("tls_fingerprint_request_success", "account_id", accountID, "status", resp.StatusCode)
|
||||
|
||||
// 如果上游返回了压缩内容,解压后再交给业务层
|
||||
decompressResponseBody(resp)
|
||||
|
||||
// 包装响应体,在关闭时自动减少计数并更新时间戳
|
||||
resp.Body = wrapTrackedBody(resp.Body, func() {
|
||||
atomic.AddInt64(&entry.inFlight, -1)
|
||||
@@ -884,3 +894,56 @@ func wrapTrackedBody(body io.ReadCloser, onClose func()) io.ReadCloser {
|
||||
}
|
||||
return &trackedBody{ReadCloser: body, onClose: onClose}
|
||||
}
|
||||
|
||||
// decompressResponseBody 根据 Content-Encoding 解压响应体。
|
||||
// 当请求显式设置了 accept-encoding 时,Go 的 Transport 不会自动解压,需要手动处理。
|
||||
// 解压成功后会删除 Content-Encoding 和 Content-Length header(长度已不准确)。
|
||||
func decompressResponseBody(resp *http.Response) {
|
||||
if resp == nil || resp.Body == nil {
|
||||
return
|
||||
}
|
||||
ce := strings.ToLower(strings.TrimSpace(resp.Header.Get("Content-Encoding")))
|
||||
if ce == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
switch ce {
|
||||
case "gzip":
|
||||
gr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return // 解压失败,保持原样
|
||||
}
|
||||
reader = gr
|
||||
case "br":
|
||||
reader = brotli.NewReader(resp.Body)
|
||||
case "deflate":
|
||||
reader = flate.NewReader(resp.Body)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
originalBody := resp.Body
|
||||
resp.Body = &decompressedBody{reader: reader, closer: originalBody}
|
||||
resp.Header.Del("Content-Encoding")
|
||||
resp.Header.Del("Content-Length") // 解压后长度不确定
|
||||
resp.ContentLength = -1
|
||||
}
|
||||
|
||||
// decompressedBody 组合解压 reader 和原始 body 的 close。
|
||||
type decompressedBody struct {
|
||||
reader io.Reader
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
func (d *decompressedBody) Read(p []byte) (int, error) {
|
||||
return d.reader.Read(p)
|
||||
}
|
||||
|
||||
func (d *decompressedBody) Close() error {
|
||||
// 如果 reader 本身也是 Closer(如 gzip.Reader),先关闭它
|
||||
if rc, ok := d.reader.(io.Closer); ok {
|
||||
_ = rc.Close()
|
||||
}
|
||||
return d.closer.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user