Skip to content

fix(http11): prevent retryablehttp HTTP/2 fallback from bypassing -pr http11 (#2240)#2447

Open
TheAuroraAI wants to merge 2 commits intoprojectdiscovery:devfrom
TheAuroraAI:fix/http11-protocol-enforcement
Open

fix(http11): prevent retryablehttp HTTP/2 fallback from bypassing -pr http11 (#2240)#2447
TheAuroraAI wants to merge 2 commits intoprojectdiscovery:devfrom
TheAuroraAI:fix/http11-protocol-enforcement

Conversation

@TheAuroraAI
Copy link

@TheAuroraAI TheAuroraAI commented Mar 9, 2026

Root Cause

When -pr http11 is specified, httpx correctly disables HTTP/2 on the main transport:

  • Sets GODEBUG=http2client=0
  • Clears transport.TLSNextProto

However, retryablehttp-go's do.go has an unconditional fallback at line 63–64:

if err != nil && stringsutil.ContainsAny(err.Error(), "malformed HTTP version \"HTTP/2\"", ...) {
    resp, err = c.HTTPClient2.Do(req.Request)
}

HTTPClient2 is a separate *http.Client configured with golang.org/x/net/http2.ConfigureTransport. Because it is set independently of the HTTP/1.1-only HTTPClient, this fallback silently bypasses the -pr http11 restriction whenever a server responds with HTTP/2 framing.

Fix

After constructing the retryablehttp client, when Protocol == "http11", replace its HTTPClient2 with the already-configured HTTP/1.1-only HTTPClient:

if httpx.Options.Protocol == "http11" {
    httpx.client.HTTPClient2 = httpx.client.HTTPClient
}

This ensures the retryablehttp fallback also honours the HTTP/1.1-only constraint without requiring any upstream changes to retryablehttp-go and without affecting client2/SupportHTTP2 for non-restricted clients.

Tests

Added TestHTTP11ProtocolEnforcement with two sub-tests:

  1. http11 mode disables HTTPClient2 fallback — asserts HTTPClient2 == HTTPClient when Protocol == "http11"
  2. default mode keeps distinct HTTPClient2 — asserts the two clients differ in default mode
=== RUN   TestHTTP11ProtocolEnforcement
=== RUN   TestHTTP11ProtocolEnforcement/http11_mode_disables_HTTPClient2_fallback
--- PASS (0.07s)
=== RUN   TestHTTP11ProtocolEnforcement/default_mode_keeps_distinct_HTTPClient2
--- PASS (0.05s)
--- PASS: TestHTTP11ProtocolEnforcement (0.12s)
PASS

Checklist

Summary by CodeRabbit

  • Bug Fixes

    • Ensures HTTP/1.1 mode is consistently enforced during HTTP client setup, preventing unintended HTTP/2 fallbacks.
  • Tests

    • Added test coverage to validate HTTP/1.1 enforcement and to confirm default protocol behavior remains unchanged.

… http11

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 (projectdiscovery#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 projectdiscovery#2240
@auto-assign auto-assign bot requested a review from dwisiswant0 March 9, 2026 17:32
@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Mar 9, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Adds t.Setenv() calls to both test functions for proper GODEBUG environment variable scoping
  • Improves test isolation by ensuring GODEBUG changes are automatically restored after each test
  • Prevents test pollution and race conditions when running tests in parallel

Comment @pdneo help for available commands. · Open in Neo

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f93edd29-f722-4013-86a2-1cef6b5f5c17

📥 Commits

Reviewing files that changed from the base of the PR and between 05f22b7 and d26a798.

📒 Files selected for processing (1)
  • common/httpx/httpx_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • common/httpx/httpx_test.go

Walkthrough

The pull request enforces HTTP/1.1-only mode by assigning the primary HTTP/1.1 client to the HTTP/2 fallback client when Options.Protocol == "http11", preventing retryablehttp-go from using an HTTP/2 fallback. The assignment is applied at two points during client initialization.

Changes

Cohort / File(s) Summary
HTTP/1.1 Protocol Enforcement
common/httpx/httpx.go
When Options.Protocol == "http11", sets HTTPClient2 = HTTPClient at two initialization points so retryablehttp-go cannot fallback to an HTTP/2 client.
Protocol Enforcement Tests
common/httpx/httpx_test.go
Adds TestHTTP11ProtocolEnforcement with subtests verifying HTTPClient and HTTPClient2 are the same instance in http11 mode and distinct by default; imports updated (os).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A client once split in two,
HTTP/1 and /2 each showed their view,
I tied their paws so they'd agree,
Now fallbacks stick to one decree —
Hop, no more surprises for you!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: preventing retryablehttp's HTTP/2 fallback from bypassing the HTTP/1.1 protocol restriction.
Linked Issues check ✅ Passed The PR successfully addresses issue #2240 by assigning HTTPClient2 = HTTPClient when Protocol is 'http11', ensuring retries respect the HTTP/1.1-only configuration and preventing unintended protocol switches.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the HTTP/1.1 protocol enforcement issue: runtime adjustments in httpx.go and corresponding test coverage in httpx_test.go.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@common/httpx/httpx_test.go`:
- Around line 14-28: The test mutates the process-global GODEBUG via New, making
the second subtest flaky; capture the original GODEBUG at the start of each
subtest (or before the first one), set or let the subtest call New as needed,
and then restore the original GODEBUG (e.g., with defer os.Setenv("GODEBUG",
orig) or os.Unsetenv if orig was empty) so the "http11 mode" and "default mode"
cases run with independent global state; update the subtests around the calls to
New (referencing New and DefaultOptions and the ht.client assertions) to save
and restore GODEBUG.
- Around line 20-23: The test is using value equality but needs pointer-identity
assertions: replace require.Equal/NotEqual checks on ht.client.HTTPClient and
ht.client.HTTPClient2 with require.Same/require.NotSame to assert they are the
same (or different) instance as described in the comment; update the assertion
calls that reference ht.client.HTTPClient and ht.client.HTTPClient2 accordingly
so the test verifies pointer identity rather than value equality.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f28db00a-c6cd-4bf9-b865-e92623c44ee7

📥 Commits

Reviewing files that changed from the base of the PR and between ed0f6af and 05f22b7.

📒 Files selected for processing (2)
  • common/httpx/httpx.go
  • common/httpx/httpx_test.go

Comment on lines +14 to +28
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Restore GODEBUG between these subtests.

Line 17 can mutate a process-global env var via New, so the "default mode" case is currently running after a polluted global state. Make each subtest restore GODEBUG so the test stays order-independent.

Suggested fix
 import (
 	"net/http"
+	"os"
 	"testing"
 
 	"github.com/projectdiscovery/retryablehttp-go"
 	"github.com/stretchr/testify/require"
 )
@@
 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)
 		require.Nil(t, err)
@@
 	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)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/httpx/httpx_test.go` around lines 14 - 28, The test mutates the
process-global GODEBUG via New, making the second subtest flaky; capture the
original GODEBUG at the start of each subtest (or before the first one), set or
let the subtest call New as needed, and then restore the original GODEBUG (e.g.,
with defer os.Setenv("GODEBUG", orig) or os.Unsetenv if orig was empty) so the
"http11 mode" and "default mode" cases run with independent global state; update
the subtests around the calls to New (referencing New and DefaultOptions and the
ht.client assertions) to save and restore GODEBUG.

…ween 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
@TheAuroraAI
Copy link
Author

Addressed CodeRabbit review:

  • Pointer-identity assertions: Replaced require.Equal/NotEqual with require.Same/NotSame on ht.client.HTTPClient vs ht.client.HTTPClient2 to correctly assert pointer identity rather than value equality.
  • GODEBUG isolation: Added t.Setenv("GODEBUG", os.Getenv("GODEBUG")) in each subtest so the New call's GODEBUG mutation in the first subtest doesn't pollute the second's global state. t.Setenv automatically restores the original value on test cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

-pr http11 flag is ignored on retryablehttp-go due to HTTP/2 fallback

1 participant