Skip to content

Comments

fix: honor -pr http11 by disabling retryable HTTP/2 fallback#2415

Open
shukwei899 wants to merge 1 commit intoprojectdiscovery:devfrom
shukwei899:fix/algora-2240-http11-no-h2-fallback
Open

fix: honor -pr http11 by disabling retryable HTTP/2 fallback#2415
shukwei899 wants to merge 1 commit intoprojectdiscovery:devfrom
shukwei899:fix/algora-2240-http11-no-h2-fallback

Conversation

@shukwei899
Copy link

@shukwei899 shukwei899 commented Feb 20, 2026

Proposed Changes

  • when -pr http11 is selected, force retryablehttp to reuse the same HTTP/1.1 client for fallback retries
  • this prevents retryablehttp from switching to its dedicated HTTP/2 client on malformed HTTP version errors
  • add focused unit tests to verify:
    • HTTP/1.1 mode uses the same client for HTTPClient and HTTPClient2
    • default mode still keeps a dedicated HTTP/2 fallback client

Proof

go test ./common/httpx -run 'TestHTTP11DisablesRetryHTTP2Fallback|TestDefaultProtocolKeepsDedicatedHTTP2Client' -count=1
ok  github.com/projectdiscovery/httpx/common/httpx  0.593s

Checklist

  • PR created against the correct branch (dev)
  • Targeted tests passed
  • Tests added for the behavior change
  • Documentation updated (not required for this internal behavior fix)

/claim #2240

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed HTTP/1.1 request handling to prevent unintended fallback to HTTP/2 when HTTP/1.1 is explicitly requested, ensuring consistent protocol usage during retries.
  • Tests

    • Added test coverage for HTTP/1.1 protocol enforcement and HTTP/2 client configuration behavior.

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Feb 20, 2026

Neo Security Audit

No security issues found

Highlights

Hardening Notes
  • The fix addresses issue -pr http11 flag is ignored on retryablehttp-go due to HTTP/2 fallback #2240 where HTTP/1.1 mode was being bypassed due to retryablehttp's automatic HTTP/2 fallback on malformed responses
  • By setting HTTPClient2 = HTTPClient when Protocol is 'http11', both the primary and fallback clients use the same HTTP/1.1-only transport
  • This prevents HTTP/2 downgrade attack vectors (H2.TE, H2.CL) where attackers exploit differences in how frontend HTTP/2 and backend HTTP/1.1 systems parse Content-Length and Transfer-Encoding headers
  • The implementation is safe: Go's http.Client is designed for concurrent use, so sharing the same instance between HTTPClient and HTTPClient2 introduces no race conditions
  • Test coverage properly validates both HTTP/1.1 mode (clients are same instance) and default mode (clients remain separate)

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

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Walkthrough

A new guard is added to the HTTP client initialization to explicitly handle HTTP/1.1 protocol requests by synchronizing the HTTP/2 client field, preventing unintended HTTP/2 fallback behavior during retries. Supporting tests validate both HTTP/1.1 and default protocol scenarios.

Changes

Cohort / File(s) Summary
HTTP/1.1 Protocol Guard
common/httpx/httpx.go
Adds conditional logic in New() function to set HTTPClient2 equal to HTTPClient when protocol is explicitly set to "http11", preventing retryablehttp from falling back to HTTP/2 on malformed response retries.
Protocol Handling Tests
common/httpx/httpx_test.go
Adds two test functions: TestHTTP11DisablesRetryHTTP2Fallback validates HTTP/1.1 explicit behavior, and TestDefaultProtocolKeepsDedicatedHTTP2Client validates that default protocol maintains distinct HTTP/2 client. Includes time package import.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Hop-hopping through protocols with glee,
HTTP/1.1, stay true, don't flee!
No sneaky fallbacks to HTTP/2,
Our guard keeps clients sincere and true!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fixing HTTP/1.1 protocol handling by disabling HTTP/2 fallback in retryable requests, which matches the core objective of the changeset.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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: 1

🧹 Nitpick comments (1)
common/httpx/httpx_test.go (1)

33-55: Add cleanup to both tests to close the fastdialer.Dialer

New() creates a fastdialer.Dialer (stored in ht.Dialer) which manages resources and should be released. Both tests are missing cleanup, which can cause resource leaks when running with -count or -race.

♻️ Proposed fix — add cleanup to both tests
 func TestHTTP11DisablesRetryHTTP2Fallback(t *testing.T) {
 	opts := DefaultOptions
 	opts.Timeout = 2 * time.Second
 	opts.Protocol = "http11"

 	ht, err := New(&opts)
 	require.NoError(t, err)
+	t.Cleanup(func() { ht.Dialer.Close() })
 	require.NotNil(t, ht.client)
 	require.NotNil(t, ht.client.HTTPClient)
 	require.Same(t, ht.client.HTTPClient, ht.client.HTTPClient2)
 }

 func TestDefaultProtocolKeepsDedicatedHTTP2Client(t *testing.T) {
 	opts := DefaultOptions
 	opts.Timeout = 2 * time.Second

 	ht, err := New(&opts)
 	require.NoError(t, err)
+	t.Cleanup(func() { ht.Dialer.Close() })
 	require.NotNil(t, ht.client)
 	require.NotNil(t, ht.client.HTTPClient)
 	require.NotNil(t, ht.client.HTTPClient2)
 	require.NotSame(t, ht.client.HTTPClient, ht.client.HTTPClient2)
 }
🤖 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 33 - 55, Both tests create an ht via
New(...) which allocates a fastdialer.Dialer at ht.Dialer but never releases it;
add cleanup to close the dialer after creation. In
TestHTTP11DisablesRetryHTTP2Fallback and
TestDefaultProtocolKeepsDedicatedHTTP2Client, after verifying err==nil and ht !=
nil, defer closing the dialer (e.g. defer ht.Dialer.Close() or check ht.Dialer
!= nil then close) so resources are always released even on test failures.
🤖 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.go`:
- Around line 186-190: The guard checking httpx.client != nil before assigning
HTTPClient2 is redundant because NewWithHTTPClient always returns a non-nil
*retryablehttp.Client; remove the nil check and simplify the block to only test
httpx.Options.Protocol == "http11" and then set httpx.client.HTTPClient2 =
httpx.client.HTTPClient (referencing httpx.Options.Protocol, httpx.client,
HTTPClient and HTTPClient2) so the intent (prevent falling back to HTTP/2) is
preserved without the unnecessary nil condition.

---

Nitpick comments:
In `@common/httpx/httpx_test.go`:
- Around line 33-55: Both tests create an ht via New(...) which allocates a
fastdialer.Dialer at ht.Dialer but never releases it; add cleanup to close the
dialer after creation. In TestHTTP11DisablesRetryHTTP2Fallback and
TestDefaultProtocolKeepsDedicatedHTTP2Client, after verifying err==nil and ht !=
nil, defer closing the dialer (e.g. defer ht.Dialer.Close() or check ht.Dialer
!= nil then close) so resources are always released even on test failures.

Comment on lines +186 to +190
// When HTTP/1.1 is explicitly requested, prevent retryablehttp from
// falling back to HTTP/2 on malformed response retries.
if httpx.Options.Protocol == "http11" && httpx.client != nil {
httpx.client.HTTPClient2 = httpx.client.HTTPClient
}
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

Redundant httpx.client != nil guard

retryablehttp.Client holds both HTTPClient and HTTPClient2 as exported fields; NewWithHTTPClient always returns a fully-initialised, non-nil pointer. The httpx.client != nil branch condition is therefore always true and adds noise without a safety benefit. The protocol check alone is sufficient:

🛠️ Proposed simplification
-	if httpx.Options.Protocol == "http11" && httpx.client != nil {
+	if httpx.Options.Protocol == "http11" {
 		httpx.client.HTTPClient2 = httpx.client.HTTPClient
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/httpx/httpx.go` around lines 186 - 190, The guard checking
httpx.client != nil before assigning HTTPClient2 is redundant because
NewWithHTTPClient always returns a non-nil *retryablehttp.Client; remove the nil
check and simplify the block to only test httpx.Options.Protocol == "http11" and
then set httpx.client.HTTPClient2 = httpx.client.HTTPClient (referencing
httpx.Options.Protocol, httpx.client, HTTPClient and HTTPClient2) so the intent
(prevent falling back to HTTP/2) is preserved without the unnecessary nil
condition.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant