fix: Enable HTTP/2 for proxy upstream with connection tuning (#183)

* fix: Enable HTTP/2 for proxy upstream with connection tuning

* fix: Handle HTTP/2 upstream translating to HTTP/1.1 downstream

* fix: Remove go tool golangci-lint as per docs

* fix: Code review fixes

* fix: Code review fixes
This commit is contained in:
Abhisek Datta
2026-03-11 14:40:27 +05:30
committed by GitHub
parent e31a11e8e2
commit 19e04b71b4
5 changed files with 102 additions and 613 deletions
+66
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewProxyServerSecuresUpstreamTLSConfig(t *testing.T) {
@@ -28,6 +29,25 @@ func TestNewProxyServerSecuresUpstreamTLSConfig(t *testing.T) {
assert.GreaterOrEqual(t, internalProxy.proxy.Tr.TLSClientConfig.MinVersion, uint16(tls.VersionTLS12), "minimum TLS version should be 1.2+")
}
func TestNewProxyServerUpstreamTransportEnablesHTTP2(t *testing.T) {
server, err := NewProxyServer(&ProxyConfig{
ListenAddr: "127.0.0.1:0",
EnableMITM: false,
ConnectTimeout: 30 * time.Second,
RequestTimeout: 5 * time.Minute,
})
assert.NoError(t, err)
internalProxy, ok := server.(*proxyServer)
assert.True(t, ok)
tr := internalProxy.proxy.Tr
assert.True(t, tr.ForceAttemptHTTP2, "HTTP/2 must be enabled to allow upstream connection multiplexing")
assert.Equal(t, 100, tr.MaxConnsPerHost, "MaxConnsPerHost should cap concurrent connections per upstream host")
assert.Equal(t, 50, tr.MaxIdleConnsPerHost, "MaxIdleConnsPerHost should be raised from default of 2 for connection reuse")
assert.Equal(t, 200, tr.MaxIdleConns, "MaxIdleConns should accommodate multiple upstream registries")
}
func TestNormalizeRequestURL(t *testing.T) {
tests := []struct {
name string
@@ -130,3 +150,49 @@ func TestNewProxyServerRejectsUntrustedUpstreamCertByDefault(t *testing.T) {
assert.Error(t, err, "untrusted upstream certificate should fail verification")
assert.Nil(t, resp)
}
func TestResponseProtoNormalisedToHTTP11(t *testing.T) {
// When the upstream transport negotiates HTTP/2, responses arrive with
// Proto "HTTP/2.0". goproxy writes MITM responses via resp.Write() which
// serialises the status line verbatim. An HTTP/1.1 client rejects the
// "HTTP/2.0 200 OK" status line and resets the connection.
// The OnResponse handler must normalise the proto back to HTTP/1.1.
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer upstream.Close()
server, err := NewProxyServer(&ProxyConfig{
ListenAddr: "127.0.0.1:0",
EnableMITM: false,
ConnectTimeout: 5 * time.Second,
RequestTimeout: 5 * time.Second,
})
assert.NoError(t, err)
ps, ok := server.(*proxyServer)
assert.True(t, ok)
// Simulate an HTTP/2 response going through the OnResponse handler by
// sending a request through the proxy to the upstream test server.
assert.NoError(t, ps.Start())
defer func() {
_ = ps.Stop(t.Context())
}()
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(&url.URL{
Scheme: "http",
Host: ps.Address(),
}),
},
}
resp, err := client.Get(upstream.URL)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, 1, resp.ProtoMajor, "response ProtoMajor should be 1 (HTTP/1.1)")
assert.Equal(t, 1, resp.ProtoMinor, "response ProtoMinor should be 1 (HTTP/1.1)")
}