From 213de0797e774dec5f028a82ef4edfea3ff85739 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Fri, 11 Sep 2026 19:19:40 +0800 Subject: [PATCH] fix(gateway): avoid duplicate Accept-Encoding headers --- .../service/gateway_accept_encoding_test.go | 81 +++++++++++++++++++ backend/internal/service/header_util.go | 3 +- 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 backend/internal/service/gateway_accept_encoding_test.go diff --git a/backend/internal/service/gateway_accept_encoding_test.go b/backend/internal/service/gateway_accept_encoding_test.go new file mode 100644 index 0000000000..7271f657ea --- /dev/null +++ b/backend/internal/service/gateway_accept_encoding_test.go @@ -0,0 +1,81 @@ +package service + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestGatewayService_AcceptEncodingOnWire(t *testing.T) { + gin.SetMode(gin.TestMode) + for _, protocol := range []struct { + name string + major int + }{ + {name: "HTTP1", major: 1}, + {name: "HTTP2", major: 2}, + } { + t.Run(protocol.name, func(t *testing.T) { + type receivedRequest struct { + encodings []string + proto int + } + received := make(chan receivedRequest, 1) + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + received <- receivedRequest{encodings: r.Header.Values("Accept-Encoding"), proto: r.ProtoMajor} + w.WriteHeader(http.StatusNoContent) + })) + server.EnableHTTP2 = protocol.major == 2 + server.StartTLS() + defer server.Close() + client := server.Client() + client.Timeout = 5 * time.Second + upstreamURL, err := url.Parse(server.URL) + require.NoError(t, err) + + for _, tc := range []struct { + name string + encoding string + want string + }{ + {name: "gzip", encoding: "gzip", want: "gzip"}, + {name: "multiple encodings", encoding: "gzip, deflate, br", want: "gzip, deflate, br"}, + {name: "identity", encoding: "identity", want: "identity"}, + {name: "omitted", want: "gzip"}, + } { + t.Run(tc.name, func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + if tc.encoding != "" { + c.Request.Header.Set("Accept-Encoding", tc.encoding) + } + svc := &GatewayService{cfg: &config.Config{}} + account := &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth} + req, _, err := svc.buildUpstreamRequest(context.Background(), c, account, + []byte(`{"model":"claude-sonnet-4-6","messages":[]}`), "test-token", "oauth", "claude-sonnet-4-6", false, false) + require.NoError(t, err) + req.URL.Scheme = upstreamURL.Scheme + req.URL.Host = upstreamURL.Host + req.Host = "" + + resp, err := client.Do(req) + require.NoError(t, err) + _, err = io.Copy(io.Discard, resp.Body) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + got := <-received + require.Equal(t, protocol.major, got.proto) + require.Equal(t, []string{tc.want}, got.encodings) + }) + } + }) + } +} diff --git a/backend/internal/service/header_util.go b/backend/internal/service/header_util.go index f8da068df5..bbb293d21e 100644 --- a/backend/internal/service/header_util.go +++ b/backend/internal/service/header_util.go @@ -6,6 +6,7 @@ import ( ) // headerWireCasing 定义每个白名单 header 在真实 Claude CLI 抓包中的准确大小写。 +// Accept-Encoding keeps canonical casing so net/http recognizes explicit compression negotiation. // Go 的 HTTP server 解析请求时会将所有 header key 转为 Canonical 形式(如 x-app → X-App), // 此 map 用于在转发时恢复到真实的 wire format。 // @@ -34,7 +35,7 @@ var headerWireCasing = map[string]string{ "content-type": "content-type", "accept-language": "accept-language", "sec-fetch-mode": "sec-fetch-mode", - "accept-encoding": "accept-encoding", + "accept-encoding": "Accept-Encoding", "authorization": "authorization", // Claude Code 2.1.87+ 新增 header