diff --git a/common/httpx/httpx.go b/common/httpx/httpx.go index 039f4c4c..491c467c 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{} @@ -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 7da6ad12..d69f7042 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") + }) +}