Skip to content

fix: respect explicit HTTP/1.1 configuration by disabling HTTP/2 fallback#2433

Open
hetbhatt2807 wants to merge 11 commits intoprojectdiscovery:devfrom
hetbhatt2807:dev
Open

fix: respect explicit HTTP/1.1 configuration by disabling HTTP/2 fallback#2433
hetbhatt2807 wants to merge 11 commits intoprojectdiscovery:devfrom
hetbhatt2807:dev

Conversation

@hetbhatt2807
Copy link

@hetbhatt2807 hetbhatt2807 commented Mar 4, 2026

This PR ensures that when -pr http11 is explicitly set,
httpx does not fallback to HTTP/2 via retryablehttp-go.

Problem:
retryablehttp-go falls back to HTTPClient2 on malformed HTTP/2 errors.
This overrides explicit HTTP/1.1-only configurations in httpx,
causing unintended protocol switches.

Solution:

  • Added DisableHTTPFallback support via retryablehttp-go
  • Wired fallback control inside HTTPX client initialization
  • Ensures strict protocol behavior when -pr http11 is used

Changes:
✓ common/httpx/options.go – added fallback control field
✓ common/httpx/retry.go – introduced fallback logic handling
✓ common/httpx/httpx.go – wired DisableHTTPFallback
✓ common/httpx/*_test.go – added unit tests
✓ README.md – documented behavior
✓ CHANGELOG.md – added entry

Testing:

  • go test ./... passes
  • Manual validation with -debug confirms no HTTP/2 fallback
  • Verified strict HTTP/1.1 behavior

This PR depends on projectdiscovery/retryablehttp-go#529.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --dhf flag to disable automatic HTTP/2 fallback, enforcing strict HTTP/1.1 protocol compliance.
  • Documentation

    • Added README section documenting the disable HTTP/2 fallback feature with usage examples.
  • Tests

    • Added comprehensive test coverage for HTTP/2 fallback disable functionality and retry behavior.

@auto-assign auto-assign bot requested a review from dogancanbakir March 4, 2026 10:20
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Walkthrough

These changes introduce a DisableHTTPFallback feature across the httpx client, runner, and options modules. When enabled with the --dhf flag and HTTP/1.1 protocol, the client prevents automatic HTTP/2 fallback retries. A custom retry policy, helper function for error detection, comprehensive tests, and documentation support this functionality.

Changes

Cohort / File(s) Summary
Configuration & Options
README.md, runner/options.go, common/httpx/option.go
Added DisableHTTPFallback option to Options struct with default false value, introduced --dhf command-line flag in runner, and documented the new flag with usage example in README.
HTTP Client & Retry Logic
common/httpx/httpx.go, common/httpx/retry.go
Implemented custom retry policy that blocks HTTP/2 fallback retries when DisableHTTPFallback is enabled and Protocol is http11. Added isHTTP2FallbackError helper to detect HTTP/2 fallback errors and NewCheckRetryFunc method to apply custom retry logic.
Testing
common/httpx/httpx_disablehttpfallback_test.go
Added comprehensive test file covering HTTP/2 fallback blocking behavior, retry behavior for non-HTTP/2 errors, and error detection validation across multiple scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RetryHandler as Retry Handler
    participant ErrorDetector as Error Detector
    
    Client->>RetryHandler: Request fails with error
    RetryHandler->>ErrorDetector: isHTTP2FallbackError(err)?
    ErrorDetector-->>RetryHandler: true/false
    
    alt DisableHTTPFallback=true AND Protocol=http11 AND HTTP/2 Fallback Error
        RetryHandler-->>Client: Retry=false, return error
    else DisableHTTPFallback=false OR Non-Fallback Error
        RetryHandler->>RetryHandler: Apply DefaultRetryPolicy
        RetryHandler-->>Client: Retry decision from default policy
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A flag to hold, a choice so clear,
HTTP/2 fallback won't appear!
With custom retry logic in place,
The rabbit's code sets a steady pace.
One feature hops across the fold,
With tests that shine and docs of gold! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 and specifically describes the main change: adding the ability to respect explicit HTTP/1.1 configuration by disabling HTTP/2 fallback, which is the core objective of this PR.

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

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

18-24: Add one constructor-path test for HTTPX.New(...) wiring.

These tests validate the retry helper path, but they don’t assert that common/httpx/httpx.go actually wires httpx.client.CheckRetry correctly when Protocol=http11 and DisableHTTPFallback=true. Add one integration-style test through New(...) to prevent regressions in the real runtime path.

Also applies to: 45-51, 69-75

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

In `@common/httpx/httpx_disablehttpfallback_test.go` around lines 18 - 24, Add an
integration-style test that constructs the HTTPX client via HTTPX.New(...) (not
the lower-level constructor) with options Protocol=http11 and
DisableHTTPFallback=true, then obtain the retry helper via the exported
NewCheckRetryFunc (or inspect httpx.client.CheckRetry) and assert it behaves as
expected (e.g., non-nil and enforces no-fallback semantics); this ensures
common/httpx/httpx.go actually wires the client's CheckRetry when using the real
New(...) path and prevents regressions.
🤖 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 475-486: The getCustomCheckRetry function is currently nested
inside the Sanitize function which causes a compile error; move the entire
getCustomCheckRetry declaration so it is a top-level (file-level) function (not
inside Sanitize), keeping its signature getCustomCheckRetry(opts *Options)
retryablehttp.CheckRetryFunc and its use of isHTTP2FallbackError and
retryablehttp.DefaultRetryPolicy intact; ensure Sanitize returns before the
function and update any references to getCustomCheckRetry to use the now
top-level function.

In `@common/httpx/retry.go`:
- Around line 8-9: The code imports two different retryablehttp packages causing
incompatible types and duplicated helpers; unify to the same import (use
github.com/projectdiscovery/retryablehttp-go) across files and update
signatures/usages accordingly: change the import in retry.go to
github.com/projectdiscovery/retryablehttp-go, ensure NewCheckRetryFunc returns
projectdiscovery's retryablehttp.CheckRetryFunc, update any callers (e.g.,
getCustomCheckRetry in httpx.go) to accept that type, and consolidate
isHTTP2FallbackError so it is referenced from the same package (or moved to a
shared/internal helper) to eliminate cross-package type mismatches.

In `@README.md`:
- Around line 292-299: The "### Disable HTTP/2 Fallback" heading breaks
list/heading nesting in README.md; replace that heading with inline explanatory
text (or a proper section header outside any list) and format the example as a
fenced bash block; specifically update the "Disable HTTP/2 Fallback" block to
read like a bullet or sentence (e.g., "Disable HTTP/2 fallback: Use `-dhf` to
prevent automatic fallback when running in HTTP/1.1 mode.") and keep the usage
example `httpx -u http://target.com -pr http11 -dhf` inside a ```bash fenced
code block so the README no longer has the invalid heading jump.

---

Nitpick comments:
In `@common/httpx/httpx_disablehttpfallback_test.go`:
- Around line 18-24: Add an integration-style test that constructs the HTTPX
client via HTTPX.New(...) (not the lower-level constructor) with options
Protocol=http11 and DisableHTTPFallback=true, then obtain the retry helper via
the exported NewCheckRetryFunc (or inspect httpx.client.CheckRetry) and assert
it behaves as expected (e.g., non-nil and enforces no-fallback semantics); this
ensures common/httpx/httpx.go actually wires the client's CheckRetry when using
the real New(...) path and prevents regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89bf6dbe-27b1-4a6f-beb4-3739b3105bca

📥 Commits

Reviewing files that changed from the base of the PR and between 4311f8b and d5a94ff.

📒 Files selected for processing (6)
  • README.md
  • common/httpx/httpx.go
  • common/httpx/httpx_disablehttpfallback_test.go
  • common/httpx/option.go
  • common/httpx/retry.go
  • runner/options.go

Comment on lines +475 to 486
// getCustomCheckRetry returns a custom CheckRetry function that respects DisableHTTPFallback
func getCustomCheckRetry(opts *Options) retryablehttp.CheckRetryFunc {
return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if opts.Protocol == "http11" && opts.DisableHTTPFallback {
if isHTTP2FallbackError(err) {
return false, err
}
}
return retryablehttp.DefaultRetryPolicy(ctx, resp, 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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Check if file exists and get its size
if [ -f "common/httpx/httpx.go" ]; then
  wc -l common/httpx/httpx.go
  # Show lines 455-510 with context around the cited lines 475-486
  nl -ba common/httpx/httpx.go | sed -n '455,510p'
else
  echo "File not found at common/httpx/httpx.go"
  find . -name "httpx.go" -type f 2>/dev/null || echo "No httpx.go found"
fi

Repository: projectdiscovery/httpx

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# View lines 455-486 using sed for line numbers
sed -n '455,486p' common/httpx/httpx.go | awk '{printf "%4d  %s\n", NR+454, $0}'

Repository: projectdiscovery/httpx

Length of output: 1178


Move getCustomCheckRetry out of Sanitize (compile blocker).

Go does not support named function declarations inside other functions. The getCustomCheckRetry function at lines 476-485 is incorrectly nested within the Sanitize method (which closes at line 486), appearing after the return statement at line 474. This is a syntax error and will not compile.

🐛 Proposed fix
 func (httpx *HTTPX) Sanitize(respStr string, trimLine, normalizeSpaces bool) string {
 	respStr = httpx.htmlPolicy.Sanitize(respStr)
 	if trimLine {
 		respStr = strings.ReplaceAll(respStr, "\n", "")
 	}
 	if normalizeSpaces {
 		respStr = httputilz.NormalizeSpaces(respStr)
 	}
 	return respStr
-	// getCustomCheckRetry returns a custom CheckRetry function that respects DisableHTTPFallback
- func getCustomCheckRetry(opts *Options) retryablehttp.CheckRetryFunc {
-	return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
-		if opts.Protocol == "http11" && opts.DisableHTTPFallback {
-			if isHTTP2FallbackError(err) {
-				return false, err
-			}
-		}
-		return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
-	}
-}
 }
+
+// getCustomCheckRetry returns a custom CheckRetry function that respects DisableHTTPFallback.
+func getCustomCheckRetry(opts *Options) retryablehttp.CheckRetryFunc {
+	return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
+		if opts.Protocol == "http11" && opts.DisableHTTPFallback && isHTTP2FallbackError(err) {
+			return false, err
+		}
+		return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/httpx/httpx.go` around lines 475 - 486, The getCustomCheckRetry
function is currently nested inside the Sanitize function which causes a compile
error; move the entire getCustomCheckRetry declaration so it is a top-level
(file-level) function (not inside Sanitize), keeping its signature
getCustomCheckRetry(opts *Options) retryablehttp.CheckRetryFunc and its use of
isHTTP2FallbackError and retryablehttp.DefaultRetryPolicy intact; ensure
Sanitize returns before the function and update any references to
getCustomCheckRetry to use the now top-level function.

Comment on lines +8 to +9
"github.com/hashicorp/go-retryablehttp"
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Retry-related imports in common/httpx ==="
rg -n 'go-retryablehttp|retryablehttp-go' common/httpx

echo
echo "=== CheckRetry assignments/usages ==="
rg -n '\bCheckRetry\b|NewCheckRetryFunc|getCustomCheckRetry' common/httpx

echo
echo "=== Client/NewClient symbols in common/httpx ==="
rg -nP 'type\s+Client\s+struct|func\s+NewClient\s*\(' common/httpx

Repository: projectdiscovery/httpx

Length of output: 1275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== common/httpx/retry.go (lines 1-35) ==="
cat -n common/httpx/retry.go | head -35

echo
echo "=== common/httpx/httpx.go (lines 180-195) ==="
sed -n '180,195p' common/httpx/httpx.go | cat -n

Repository: projectdiscovery/httpx

Length of output: 1656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking where NewCheckRetryFunc is used ==="
rg -n 'NewCheckRetryFunc' --type go

echo
echo "=== getCustomCheckRetry implementation (lines 475-500) ==="
sed -n '475,500p' common/httpx/httpx.go | cat -n

echo
echo "=== Checking imports in httpx.go (first 30 lines) ==="
head -30 common/httpx/httpx.go | cat -n

Repository: projectdiscovery/httpx

Length of output: 2134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking if isHTTP2FallbackError is in retry.go ==="
rg -n 'isHTTP2FallbackError' common/httpx

echo
echo "=== Checking DefaultRetryPolicy calls ==="
rg -n 'DefaultRetryPolicy' common/httpx

Repository: projectdiscovery/httpx

Length of output: 609


Unify retryablehttp import path to avoid type incompatibility.

retry.go imports github.com/hashicorp/go-retryablehttp while httpx.go and other files use github.com/projectdiscovery/retryablehttp-go. This creates type drift: NewCheckRetryFunc() returns CheckRetryFunc from the hashicorp package, but getCustomCheckRetry() (in httpx.go) returns CheckRetryFunc from the projectdiscovery package—these are incompatible types even though they share the same signature. Additionally, isHTTP2FallbackError() is defined in retry.go but also used in httpx.go, creating inconsistent package references.

🔧 Proposed fix
 import (
 	"context"
 	"net/http"
 	"strings"
 
-	"github.com/hashicorp/go-retryablehttp"
+	retryablehttp "github.com/projectdiscovery/retryablehttp-go"
 )
📝 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
"github.com/hashicorp/go-retryablehttp"
)
import (
"context"
"net/http"
"strings"
retryablehttp "github.com/projectdiscovery/retryablehttp-go"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/httpx/retry.go` around lines 8 - 9, The code imports two different
retryablehttp packages causing incompatible types and duplicated helpers; unify
to the same import (use github.com/projectdiscovery/retryablehttp-go) across
files and update signatures/usages accordingly: change the import in retry.go to
github.com/projectdiscovery/retryablehttp-go, ensure NewCheckRetryFunc returns
projectdiscovery's retryablehttp.CheckRetryFunc, update any callers (e.g.,
getCustomCheckRetry in httpx.go) to accept that type, and consolidate
isHTTP2FallbackError so it is referenced from the same package (or moved to a
shared/internal helper) to eliminate cross-package type mismatches.

Comment on lines +292 to +299
- ### Disable HTTP/2 Fallback

Use the `-dhf` flag to prevent automatic fallback to HTTP/2 when using HTTP/1.1 mode:
```bash
httpx -u http://target.com -pr http11 -dhf
```

This ensures strict HTTP/1.1 protocol compliance and prevents automatic protocol switching during retries.
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

Fix heading nesting in Notes.

The - ### Disable HTTP/2 Fallback block introduces an invalid heading jump and odd list/heading structure. Convert it to a normal bullet (or promote to a proper section heading outside the list).

📝 Proposed markdown fix
-- ### Disable HTTP/2 Fallback
-
-Use the `-dhf` flag to prevent automatic fallback to HTTP/2 when using HTTP/1.1 mode:
-```bash
-httpx -u http://target.com -pr http11 -dhf
-```
-
-This ensures strict HTTP/1.1 protocol compliance and prevents automatic protocol switching during retries.
+- **Disable HTTP/2 fallback:** Use `-dhf` to prevent automatic fallback to HTTP/2 when running in HTTP/1.1 mode.
+  ```bash
+  httpx -u http://target.com -pr http11 -dhf
+  ```
+  This enforces strict HTTP/1.1 behavior during retries.
📝 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
- ### Disable HTTP/2 Fallback
Use the `-dhf` flag to prevent automatic fallback to HTTP/2 when using HTTP/1.1 mode:
```bash
httpx -u http://target.com -pr http11 -dhf
```
This ensures strict HTTP/1.1 protocol compliance and prevents automatic protocol switching during retries.
- **Disable HTTP/2 fallback:** Use `-dhf` to prevent automatic fallback to HTTP/2 when running in HTTP/1.1 mode.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 292-292: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

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

In `@README.md` around lines 292 - 299, The "### Disable HTTP/2 Fallback" heading
breaks list/heading nesting in README.md; replace that heading with inline
explanatory text (or a proper section header outside any list) and format the
example as a fenced bash block; specifically update the "Disable HTTP/2
Fallback" block to read like a bullet or sentence (e.g., "Disable HTTP/2
fallback: Use `-dhf` to prevent automatic fallback when running in HTTP/1.1
mode.") and keep the usage example `httpx -u http://target.com -pr http11 -dhf`
inside a ```bash fenced code block so the README no longer has the invalid
heading jump.

Copy link
Member

@Mzack9999 Mzack9999 left a comment

Choose a reason for hiding this comment

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

Requested changes in the associated PR in projectdiscovery/retryablehttp-go#529

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.

2 participants