From 671d2959970101b74d76c8e0bdf85c32bde073ae Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Tue, 10 Mar 2026 09:54:30 -0700 Subject: [PATCH 1/2] fix: preserve -pr http11 across retryablehttp-go fallback (#2240) retryablehttp-go falls back to HTTPClient2 (HTTP/2-capable) when it hits a "malformed HTTP version" error, silently upgrading the protocol even when -pr http11 is set. Point HTTPClient2 at the same HTTP/1.1-only client to neutralise the fallback. Fixes #2240 --- common/httpx/httpx.go | 9 +++++++++ common/httpx/httpx_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/common/httpx/httpx.go b/common/httpx/httpx.go index 039f4c4ca..cc49cf71c 100644 --- a/common/httpx/httpx.go +++ b/common/httpx/httpx.go @@ -183,6 +183,15 @@ func New(options *Options) (*HTTPX, error) { CheckRedirect: redirectFunc, }, retryablehttpOptions) + // When HTTP/1.1-only mode is enforced via -pr http11, prevent retryablehttp-go + // from silently upgrading to HTTP/2 on retry. retryablehttp-go falls back to + // HTTPClient2 (an HTTP/2-capable client) when it encounters "malformed HTTP + // version" errors from servers that speak HTTP/2. Pointing HTTPClient2 at the + // same HTTP/1.1-only client neutralises the fallback. See: #2240 + if httpx.Options.Protocol == "http11" { + httpx.client.HTTPClient2 = httpx.client.HTTPClient + } + transport2 := &http2.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, diff --git a/common/httpx/httpx_test.go b/common/httpx/httpx_test.go index 7da6ad12d..d69f70422 100644 --- a/common/httpx/httpx_test.go +++ b/common/httpx/httpx_test.go @@ -28,3 +28,29 @@ func TestDo(t *testing.T) { require.Greater(t, len(resp.Raw), 800) }) } + +// TestHTTP11ProtocolEnforcement verifies that -pr http11 prevents retryablehttp-go's +// HTTP/2 fallback from bypassing the protocol restriction (#2240). +func TestHTTP11ProtocolEnforcement(t *testing.T) { + t.Run("http11 mode neutralises HTTPClient2 fallback", func(t *testing.T) { + opts := DefaultOptions + opts.Protocol = HTTP11 + ht, err := New(&opts) + require.Nil(t, err) + + // HTTPClient2 must be the same as HTTPClient so the fallback + // path still uses HTTP/1.1-only transport. + require.Same(t, ht.client.HTTPClient, ht.client.HTTPClient2, + "HTTPClient2 must equal HTTPClient in http11 mode to prevent HTTP/2 fallback") + }) + + t.Run("default mode keeps separate HTTPClient2 for HTTP/2", func(t *testing.T) { + ht, err := New(&DefaultOptions) + require.Nil(t, err) + + // In default mode the two clients must be distinct — HTTPClient2 + // is the HTTP/2-capable client used for protocol detection/fallback. + require.NotSame(t, ht.client.HTTPClient, ht.client.HTTPClient2, + "HTTPClient2 must differ from HTTPClient in default mode") + }) +} From b4b282965c4d4d36a1521df794502d92b4dc44b0 Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Tue, 10 Mar 2026 14:08:35 -0700 Subject: [PATCH 2/2] refactor: use HTTP11 constant instead of string literal per review --- common/httpx/httpx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/httpx/httpx.go b/common/httpx/httpx.go index cc49cf71c..491c467c2 100644 --- a/common/httpx/httpx.go +++ b/common/httpx/httpx.go @@ -153,7 +153,7 @@ func New(options *Options) (*HTTPX, error) { DisableKeepAlives: true, } - if httpx.Options.Protocol == "http11" { + if httpx.Options.Protocol == HTTP11 { // disable http2 _ = os.Setenv("GODEBUG", "http2client=0") transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} @@ -188,7 +188,7 @@ func New(options *Options) (*HTTPX, error) { // HTTPClient2 (an HTTP/2-capable client) when it encounters "malformed HTTP // version" errors from servers that speak HTTP/2. Pointing HTTPClient2 at the // same HTTP/1.1-only client neutralises the fallback. See: #2240 - if httpx.Options.Protocol == "http11" { + if httpx.Options.Protocol == HTTP11 { httpx.client.HTTPClient2 = httpx.client.HTTPClient }