Skip to content

fix: respect -pr http11 flag by preventing HTTP/2 fallback#2420

Open
Bushi-gg wants to merge 2 commits intoprojectdiscovery:devfrom
Bushi-gg:fix/2240-http11-protocol-enforcement
Open

fix: respect -pr http11 flag by preventing HTTP/2 fallback#2420
Bushi-gg wants to merge 2 commits intoprojectdiscovery:devfrom
Bushi-gg:fix/2240-http11-protocol-enforcement

Conversation

@Bushi-gg
Copy link

@Bushi-gg Bushi-gg commented Feb 24, 2026

Proposed changes

Fixes #2240
/claim #2240

When -pr http11 is set, httpx disables HTTP/2 on the main transport but retryablehttp-go still falls back to its internal HTTPClient2 (configured with HTTP/2) whenever it encounters malformed HTTP version errors in do.go. This effectively ignores the user's protocol preference.

This patch points HTTPClient2 to the same HTTP/1.1 client when Protocol == HTTP11, so the fallback path stays on HTTP/1.1. Also replaces the string literal with the existing HTTP11 constant.

Proof

$ go test ./common/httpx/ -run 'TestHTTP11|TestDefaultProtocol' -v -race -count=1
=== RUN   TestHTTP11DisablesHTTP2Fallback
--- PASS: TestHTTP11DisablesHTTP2Fallback (0.17s)
=== RUN   TestDefaultProtocolKeepsHTTP2Fallback
--- PASS: TestDefaultProtocolKeepsHTTP2Fallback (0.20s)
PASS
ok  github.com/projectdiscovery/httpx/common/httpx  2.104s

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • Bug Fixes

    • Prevented unintended HTTP/2 fallback when HTTP/1.1 is explicitly selected, ensuring consistent protocol behavior and avoiding protocol upgrades on certain network errors.
  • Tests

    • Added tests verifying that HTTP/1.1 selection disables HTTP/2 fallback and that default settings retain HTTP/2 fallback.
  • Documentation

    • Added descriptive comments clarifying protocol type constants.

@neo-by-projectdiscovery-dev
Copy link

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

Neo - PR Security Review

No security issues found

Highlights

  • Added documentation comments explaining HTTP/1.1 protocol enforcement and fallback prevention
  • Documented Proto constants to clarify protocol behavior (HTTP11, HTTP2, HTTP3)
  • Improved test isolation by properly managing GODEBUG environment variable
Hardening Notes
  • The TODO comment on line 193 of httpx.go correctly identifies that the current workaround (aliasing HTTPClient2 to HTTPClient) should eventually be replaced with a proper DisableHTTPFallback option in retryablehttp-go upstream
  • The documentation additions improve code maintainability by explaining the security rationale behind the HTTP/1.1 enforcement fix

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

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eaa9ff4 and 396a3e5.

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

Walkthrough

Replaced string protocol checks with the HTTP11 constant, added post-initialization logic to alias the HTTP/2 fallback client to the primary client when HTTP/1.1 is selected, and added tests and documentation comments to verify and describe protocol constants.

Changes

Cohort / File(s) Summary
HTTP/1.1 enforcement & client aliasing
common/httpx/httpx.go
Use HTTP11 constant instead of string; disable HTTP/2 at transport and set client.HTTPClient2 = client.HTTPClient when HTTP11 is selected to prevent retryablehttp-go from switching to an internal HTTP/2 client.
Tests: fallback behavior
common/httpx/httpx_test.go
Import os, initialize local opts := DefaultOptions, and add TestHTTP11DisablesHTTP2Fallback and TestDefaultProtocolKeepsHTTP2Fallback to assert client/HTTPClient2 aliasing behavior for HTTP/1.1 vs default.
Protocol docs
common/httpx/proto.go
Added inline documentation comments for Proto type and constants (UNKNOWN, HTTP11, HTTP2, HTTP3) with no behavioral changes.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant CLI
participant httpx as httpx (client)
participant Transport
participant retryable as retryablehttp-go
CLI->>httpx: initialize with Protocol = HTTP11
httpx->>Transport: set TLSNextProto = {} and GODEBUG=http2client=0
httpx->>httpx: set client.HTTPClient2 = client.HTTPClient (alias)
CLI->>httpx: perform request
httpx->>Transport: Do(req) using client.HTTPClient
alt malformed HTTP/2 error occurs
retryable-->>httpx: would call client.HTTPClient2 but it's aliased to HTTPClient
httpx->>Transport: retry uses same HTTP/1.1 client (no HTTP/2 fallback)
end

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I thumped my foot and tightened the thread,
I told the clients, "Stay HTTP/1.1 instead."
No sneaky fallback to a two-lined dream,
Tests hop in place and confirm the scheme.
🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preventing HTTP/2 fallback when the HTTP/1.1 flag is set.
Linked Issues check ✅ Passed The PR directly addresses issue #2240 by preventing retryablehttp-go's HTTP/2 fallback when HTTP/1.1 is explicitly requested.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing HTTP/1.1 protocol enforcement: implementation updates, comprehensive unit tests, and documentation clarifications.

✏️ 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: 2

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

156-160: Consider consolidating the two Protocol == HTTP11 blocks.

There are now two separate if httpx.Options.Protocol == HTTP11 checks at lines 156–160 and lines 188–190. The second block can't be merged with the first (since httpx.client doesn't exist yet at line 156), but it could carry a comment to that effect so it's clear why the split is intentional.

Additionally, the issue (#2240) explicitly suggested adding an upstream Options.DisableHTTPFallback field in retryablehttp-go as the cleaner long-term fix. Aliasing HTTPClient2 to HTTPClient is a functional workaround but couples httpx to the internal layout of retryablehttp-go's fallback path. If retryablehttp-go later adds a DisableHTTPFallback option, this workaround should be replaced.

Also applies to: 188-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/httpx/httpx.go` around lines 156 - 160, There are two separate checks
for httpx.Options.Protocol == HTTP11 (one that sets GODEBUG and
transport.TLSNextProto and another later that aliases HTTPClient2) which is
intentional because httpx.client doesn't exist yet in the first block;
consolidate intent by adding a clear comment to the earlier block explaining why
the second check is needed later, and update the later block (where HTTPClient2
is aliased to HTTPClient) to include a TODO referencing issue `#2240` and Prefer
adding a DisableHTTPFallback option in retryablehttp-go; this keeps the current
workaround (HTTPClient2 alias) but documents the coupling and the planned
migration target (DisableHTTPFallback) so future maintainers know to replace the
alias when that upstream option exists.
🤖 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 45-53: The test passes a pointer to the package-level
DefaultOptions into New (TestDefaultProtocolKeepsHTTP2Fallback), which New
stores and then mutates via parseCustomCookies, causing shared mutable state
across tests; fix by creating a local copy of DefaultOptions inside the test
(e.g., opts := DefaultOptions) and pass &opts to New so New and
httpx.Options.parseCustomCookies operate on a test-local struct instead of the
package-level DefaultOptions; do the same pattern for other tests like TestDo
that currently pass &DefaultOptions.
- Around line 32-43: TestHTTP11DisablesHTTP2Fallback leaks a global GODEBUG
change because New(&opts) calls os.Setenv; to prevent cross-test pollution
capture the current GODEBUG with prev := os.Getenv("GODEBUG") and after
constructing ht call t.Setenv("GODEBUG", prev) so the environment is restored
automatically by the test harness; reference TestHTTP11DisablesHTTP2Fallback and
New in your change (or alternatively remove the global os.Setenv in httpx.go as
a larger refactor).

---

Nitpick comments:
In `@common/httpx/httpx.go`:
- Around line 156-160: There are two separate checks for httpx.Options.Protocol
== HTTP11 (one that sets GODEBUG and transport.TLSNextProto and another later
that aliases HTTPClient2) which is intentional because httpx.client doesn't
exist yet in the first block; consolidate intent by adding a clear comment to
the earlier block explaining why the second check is needed later, and update
the later block (where HTTPClient2 is aliased to HTTPClient) to include a TODO
referencing issue `#2240` and Prefer adding a DisableHTTPFallback option in
retryablehttp-go; this keeps the current workaround (HTTPClient2 alias) but
documents the coupling and the planned migration target (DisableHTTPFallback) so
future maintainers know to replace the alias when that upstream option exists.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b23103e and eaa9ff4.

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

Comment on lines 32 to 43
func TestHTTP11DisablesHTTP2Fallback(t *testing.T) {
opts := DefaultOptions
opts.Protocol = HTTP11

ht, err := New(&opts)
require.Nil(t, err)

// When http11 is requested, HTTPClient2 must point to the same client
// as HTTPClient so retryablehttp-go's fallback path does not switch to HTTP/2.
require.Same(t, ht.client.HTTPClient, ht.client.HTTPClient2,
"HTTPClient2 should equal HTTPClient when Protocol is HTTP11")
}
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

os.Setenv("GODEBUG", ...) side effect leaks across tests — use t.Setenv instead.

New(&opts) with Protocol == HTTP11 calls os.Setenv("GODEBUG", "http2client=0") (line 158 of httpx.go), which mutates the global process environment and is never restored. If TestHTTP11DisablesHTTP2Fallback runs before TestDo, it will disable HTTP/2 for the live requests in TestDo, potentially changing their behavior in a non-obvious way.

Replace the os.Setenv call in httpx.go with t.Setenv inside the test, or (more practically) clean up the env var after construction in the test:

🛡️ Proposed fix to restore the environment variable
 func TestHTTP11DisablesHTTP2Fallback(t *testing.T) {
+	prevGodebug := os.Getenv("GODEBUG")
+	t.Cleanup(func() { os.Setenv("GODEBUG", prevGodebug) })
+
 	opts := DefaultOptions
 	opts.Protocol = HTTP11

Alternatively, the os.Setenv call inside httpx.go should itself be wrapped in a mechanism that can be undone per-instance (e.g., only set when no previous value was present, or tracked for cleanup), but that's a broader refactor of the pre-existing code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestHTTP11DisablesHTTP2Fallback(t *testing.T) {
opts := DefaultOptions
opts.Protocol = HTTP11
ht, err := New(&opts)
require.Nil(t, err)
// When http11 is requested, HTTPClient2 must point to the same client
// as HTTPClient so retryablehttp-go's fallback path does not switch to HTTP/2.
require.Same(t, ht.client.HTTPClient, ht.client.HTTPClient2,
"HTTPClient2 should equal HTTPClient when Protocol is HTTP11")
}
func TestHTTP11DisablesHTTP2Fallback(t *testing.T) {
prevGodebug := os.Getenv("GODEBUG")
t.Cleanup(func() { os.Setenv("GODEBUG", prevGodebug) })
opts := DefaultOptions
opts.Protocol = HTTP11
ht, err := New(&opts)
require.Nil(t, err)
// When http11 is requested, HTTPClient2 must point to the same client
// as HTTPClient so retryablehttp-go's fallback path does not switch to HTTP/2.
require.Same(t, ht.client.HTTPClient, ht.client.HTTPClient2,
"HTTPClient2 should equal HTTPClient when Protocol is HTTP11")
}
🤖 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 32 - 43,
TestHTTP11DisablesHTTP2Fallback leaks a global GODEBUG change because New(&opts)
calls os.Setenv; to prevent cross-test pollution capture the current GODEBUG
with prev := os.Getenv("GODEBUG") and after constructing ht call
t.Setenv("GODEBUG", prev) so the environment is restored automatically by the
test harness; reference TestHTTP11DisablesHTTP2Fallback and New in your change
(or alternatively remove the global os.Setenv in httpx.go as a larger refactor).

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.

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

1 participant