From 05f22b7ba429374914272527246057667159ce4b Mon Sep 17 00:00:00 2001 From: TheAuroraAI Date: Mon, 9 Mar 2026 17:32:16 +0000 Subject: [PATCH 1/2] fix(http11): prevent retryablehttp HTTP/2 fallback from bypassing -pr http11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `-pr http11` is specified, httpx correctly disables HTTP/2 on the main transport (GODEBUG=http2client=0, TLSNextProto cleared). However, retryablehttp-go's do.go falls back to HTTPClient2 — a separate HTTP/2 client — whenever it receives a "malformed HTTP version" error from a server that speaks HTTP/2 natively. This bypass caused the `-pr http11` flag to be silently ignored in practice (#2240). Fix: after constructing the retryablehttp client, replace its HTTPClient2 with the already-configured HTTP/1.1-only HTTPClient when Protocol == "http11". This neutralises the fallback without requiring any upstream changes to retryablehttp-go and without affecting HTTP/2 detection (client2/SupportHTTP2) for non-restricted clients. Adds a unit test that asserts HTTPClient2 == HTTPClient in http11 mode and HTTPClient2 != HTTPClient in default mode. 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..85cda916f 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, prevent retryablehttp-go's HTTP/2 fallback + // from bypassing the protocol restriction. retryablehttp-go falls back to HTTPClient2 + // (an HTTP/2-capable client) when it encounters a "malformed HTTP version" error from + // a server that speaks HTTP/2. Replacing HTTPClient2 with the HTTP/1.1-only HTTPClient + // ensures the -pr http11 flag is honoured end-to-end. 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..a90eba25f 100644 --- a/common/httpx/httpx_test.go +++ b/common/httpx/httpx_test.go @@ -8,6 +8,32 @@ import ( "github.com/stretchr/testify/require" ) +// TestHTTP11ProtocolEnforcement verifies that -pr http11 prevents the retryablehttp-go +// HTTP/2 fallback from bypassing the HTTP/1.1-only restriction (#2240). +func TestHTTP11ProtocolEnforcement(t *testing.T) { + t.Run("http11 mode disables HTTPClient2 fallback", func(t *testing.T) { + opts := DefaultOptions + opts.Protocol = "http11" + ht, err := New(&opts) + require.Nil(t, err) + + // When Protocol == "http11", HTTPClient2 must point to the same underlying + // client as HTTPClient so the retryablehttp-go fallback is neutralised. + require.Equal(t, ht.client.HTTPClient, ht.client.HTTPClient2, + "HTTPClient2 must equal HTTPClient in http11 mode to prevent HTTP/2 fallback") + }) + + t.Run("default mode keeps distinct HTTPClient2", func(t *testing.T) { + ht, err := New(&DefaultOptions) + require.Nil(t, err) + + // Without an explicit http11 restriction the two clients must differ + // (HTTPClient2 is the native HTTP/2 client used for protocol detection). + require.NotEqual(t, ht.client.HTTPClient, ht.client.HTTPClient2, + "HTTPClient2 must be distinct from HTTPClient in default mode") + }) +} + func TestDo(t *testing.T) { ht, err := New(&DefaultOptions) require.Nil(t, err) From d26a79847c8e037593b1fc8511bfde861acafb8a Mon Sep 17 00:00:00 2001 From: TheAuroraAI Date: Mon, 9 Mar 2026 18:15:54 +0000 Subject: [PATCH 2/2] test(http11): use pointer-identity assertions and restore GODEBUG between subtests - Replace require.Equal/NotEqual with require.Same/NotSame for pointer identity semantics (HTTPClient2 vs HTTPClient instance comparison) - Add t.Setenv("GODEBUG", ...) to each subtest to prevent GODEBUG mutation in one subtest from polluting the next --- common/httpx/httpx_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/httpx/httpx_test.go b/common/httpx/httpx_test.go index a90eba25f..153b7dd32 100644 --- a/common/httpx/httpx_test.go +++ b/common/httpx/httpx_test.go @@ -2,6 +2,7 @@ package httpx import ( "net/http" + "os" "testing" "github.com/projectdiscovery/retryablehttp-go" @@ -12,6 +13,7 @@ import ( // HTTP/2 fallback from bypassing the HTTP/1.1-only restriction (#2240). func TestHTTP11ProtocolEnforcement(t *testing.T) { t.Run("http11 mode disables HTTPClient2 fallback", func(t *testing.T) { + t.Setenv("GODEBUG", os.Getenv("GODEBUG")) opts := DefaultOptions opts.Protocol = "http11" ht, err := New(&opts) @@ -19,17 +21,18 @@ func TestHTTP11ProtocolEnforcement(t *testing.T) { // When Protocol == "http11", HTTPClient2 must point to the same underlying // client as HTTPClient so the retryablehttp-go fallback is neutralised. - require.Equal(t, ht.client.HTTPClient, ht.client.HTTPClient2, + 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 distinct HTTPClient2", func(t *testing.T) { + t.Setenv("GODEBUG", os.Getenv("GODEBUG")) ht, err := New(&DefaultOptions) require.Nil(t, err) // Without an explicit http11 restriction the two clients must differ // (HTTPClient2 is the native HTTP/2 client used for protocol detection). - require.NotEqual(t, ht.client.HTTPClient, ht.client.HTTPClient2, + require.NotSame(t, ht.client.HTTPClient, ht.client.HTTPClient2, "HTTPClient2 must be distinct from HTTPClient in default mode") }) }