Skip to content

Commit 292ec9d

Browse files
committed
Disable automatic gzip compression to fix SSE streaming
Go's default HTTP transport adds Accept-Encoding: gzip and transparently decompresses responses. This is incompatible with SSE streaming because gzip requires buffering the complete payload before decompression, causing "unexpected end of JSON input" errors with providers like OpenRouter. Clone the default transport with DisableCompression=true in NewHTTPClient, and consolidate the header tests into a single table-driven test with a shared helper. Fixes #1956 Assisted-By: cagent
1 parent 7d1a4eb commit 292ec9d

2 files changed

Lines changed: 57 additions & 51 deletions

File tree

pkg/httpclient/client.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ func NewHTTPClient(opts ...Opt) *http.Client {
2929
// Enforce a consistent User-Agent header
3030
httpOptions.Header.Set("User-Agent", fmt.Sprintf("Cagent/%s (%s; %s)", version.Version, runtime.GOOS, runtime.GOARCH))
3131

32+
// Disable automatic gzip: Go's default transport transparently compresses
33+
// and decompresses responses, which is incompatible with SSE streaming.
34+
// See https://github.com/docker/docker-agent/issues/1956
35+
rt := newTransport()
36+
3237
return &http.Client{
3338
Transport: &userAgentTransport{
3439
httpOptions: httpOptions,
35-
rt: http.DefaultTransport,
40+
rt: rt,
3641
},
3742
}
3843
}
@@ -90,6 +95,17 @@ func WithQuery(query url.Values) Opt {
9095
}
9196
}
9297

98+
// newTransport returns an HTTP transport with automatic gzip compression disabled.
99+
func newTransport() http.RoundTripper {
100+
t, ok := http.DefaultTransport.(*http.Transport)
101+
if !ok {
102+
return http.DefaultTransport
103+
}
104+
transport := t.Clone()
105+
transport.DisableCompression = true
106+
return transport
107+
}
108+
93109
type userAgentTransport struct {
94110
httpOptions HTTPOptions
95111
rt http.RoundTripper

pkg/httpclient/client_test.go

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,89 +9,79 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12-
func TestWithModelName(t *testing.T) {
12+
func TestHeaders(t *testing.T) {
1313
t.Parallel()
1414

1515
tests := []struct {
16-
name string
17-
modelName string
18-
wantSet bool
16+
name string
17+
opts []Opt
18+
wantHeader string
19+
wantValue string
1920
}{
2021
{
21-
name: "sets header when name is provided",
22-
modelName: "my-fast-model",
23-
wantSet: true,
22+
name: "WithModel sets X-Cagent-Model",
23+
opts: []Opt{WithModel("gpt-4o")},
24+
wantHeader: "X-Cagent-Model",
25+
wantValue: "gpt-4o",
2426
},
2527
{
26-
name: "skips header when name is empty",
27-
modelName: "",
28-
wantSet: false,
28+
name: "WithModelName sets X-Cagent-Model-Name",
29+
opts: []Opt{WithModelName("my-fast-model")},
30+
wantHeader: "X-Cagent-Model-Name",
31+
wantValue: "my-fast-model",
32+
},
33+
{
34+
name: "WithModelName skips header when empty",
35+
opts: []Opt{WithModelName("")},
36+
wantHeader: "X-Cagent-Model-Name",
37+
wantValue: "",
38+
},
39+
{
40+
name: "WithProvider sets X-Cagent-Provider",
41+
opts: []Opt{WithProvider("openai")},
42+
wantHeader: "X-Cagent-Provider",
43+
wantValue: "openai",
44+
},
45+
{
46+
name: "compression is disabled to support SSE streaming",
47+
wantHeader: "Accept-Encoding",
48+
wantValue: "",
2949
},
3050
}
3151

3252
for _, tt := range tests {
3353
t.Run(tt.name, func(t *testing.T) {
3454
t.Parallel()
3555

36-
var capturedHeaders http.Header
37-
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
38-
capturedHeaders = r.Header
39-
}))
40-
defer srv.Close()
41-
42-
client := NewHTTPClient(WithModelName(tt.modelName))
43-
req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody)
44-
require.NoError(t, err)
56+
headers := doRequest(t, tt.opts...)
4557

46-
resp, err := client.Do(req)
47-
require.NoError(t, err)
48-
defer func() { _ = resp.Body.Close() }()
49-
50-
if tt.wantSet {
51-
assert.Equal(t, tt.modelName, capturedHeaders.Get("X-Cagent-Model-Name"))
58+
if tt.wantValue != "" {
59+
assert.Equal(t, tt.wantValue, headers.Get(tt.wantHeader))
5260
} else {
53-
assert.Empty(t, capturedHeaders.Get("X-Cagent-Model-Name"))
61+
assert.Empty(t, headers.Get(tt.wantHeader))
5462
}
5563
})
5664
}
5765
}
5866

59-
func TestWithModel(t *testing.T) {
60-
t.Parallel()
61-
62-
var capturedHeaders http.Header
63-
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
64-
capturedHeaders = r.Header
65-
}))
66-
defer srv.Close()
67-
68-
client := NewHTTPClient(WithModel("gpt-4o"))
69-
req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody)
70-
require.NoError(t, err)
71-
72-
resp, err := client.Do(req)
73-
require.NoError(t, err)
74-
defer func() { _ = resp.Body.Close() }()
75-
76-
assert.Equal(t, "gpt-4o", capturedHeaders.Get("X-Cagent-Model"))
77-
}
78-
79-
func TestWithProvider(t *testing.T) {
80-
t.Parallel()
67+
// doRequest creates an HTTP client with the given options, sends a GET request
68+
// to a test server, and returns the headers the server received.
69+
func doRequest(t *testing.T, opts ...Opt) http.Header {
70+
t.Helper()
8171

8272
var capturedHeaders http.Header
8373
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
8474
capturedHeaders = r.Header
8575
}))
8676
defer srv.Close()
8777

88-
client := NewHTTPClient(WithProvider("openai"))
78+
client := NewHTTPClient(opts...)
8979
req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody)
9080
require.NoError(t, err)
9181

9282
resp, err := client.Do(req)
9383
require.NoError(t, err)
9484
defer func() { _ = resp.Body.Close() }()
9585

96-
assert.Equal(t, "openai", capturedHeaders.Get("X-Cagent-Provider"))
86+
return capturedHeaders
9787
}

0 commit comments

Comments
 (0)