Skip to content
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ For details about running httpx, see https://docs.projectdiscovery.io/tools/http
- As default, `httpx` probe with **HTTPS** scheme and fall-back to **HTTP** only if **HTTPS** is not reachable.
- Burp Suite XML exports can be used as input with `-l burp-export.xml -im burp`
- The `-no-fallback` flag can be used to probe and display both **HTTP** and **HTTPS** result.
- ### 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.
Comment on lines +292 to +299
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.

- Custom scheme for ports can be defined, for example `-ports http:443,http:80,https:8443`
- Custom resolver supports multiple protocol (**doh|tcp|udp**) in form of `protocol:resolver:port` (e.g. `udp:127.0.0.1:53`)
- Secret files can be used for domain-based authentication via `-sf secrets.yaml`. Supported auth types: `BasicAuth`, `BearerToken`, `Header`, `Cookie`, `Query`. Example:
Expand Down Expand Up @@ -336,4 +344,4 @@ Probing feature is inspired by [@tomnomnom/httprobe](https://github.com/tomnomno

<a href="https://discord.gg/projectdiscovery"><img src="https://raw.githubusercontent.com/projectdiscovery/nuclei-burp-plugin/main/static/join-discord.png" width="300" alt="Join Discord"></a>

</div>
</div>
15 changes: 15 additions & 0 deletions common/httpx/httpx.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ func New(options *Options) (*HTTPX, error) {
CheckRedirect: redirectFunc,
}, retryablehttpOptions)

if httpx.Options.Protocol == "http11" && httpx.Options.DisableHTTPFallback {
httpx.client.CheckRetry = getCustomCheckRetry(httpx.Options)
}

transport2 := &http2.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
Expand Down Expand Up @@ -468,4 +472,15 @@ func (httpx *HTTPX) Sanitize(respStr string, trimLine, normalizeSpaces bool) str
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)
}
}
}
Comment on lines +475 to 486
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.

123 changes: 123 additions & 0 deletions common/httpx/httpx_disablehttpfallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package httpx

import (
"context"
"errors"
"testing"
"time"
)

func TestDisableHTTPFallbackBlocks(t *testing.T) {
opts := &Options{
Protocol: "http11",
DisableHTTPFallback: true,
Timeout: 5 * time.Second,
RetryMax: 3,
}

client, err := NewClient(opts)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

checkRetry := client.NewCheckRetryFunc()
http2Error := errors.New(
"net/http: HTTP/1.x transport connection broken: malformed HTTP version \"HTTP/2\"")

shouldRetry, returnedErr := checkRetry(context.Background(), nil, http2Error)

if shouldRetry {
t.Error("Expected shouldRetry=false when DisableHTTPFallback=true")
}
if returnedErr == nil {
t.Error("Expected error to be returned")
}
}

func TestDisableHTTPFallbackAllows(t *testing.T) {
opts := &Options{
Protocol: "http11",
DisableHTTPFallback: false,
Timeout: 5 * time.Second,
RetryMax: 3,
}

client, err := NewClient(opts)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

checkRetry := client.NewCheckRetryFunc()
http2Error := errors.New(
"net/http: HTTP/1.x transport connection broken: malformed HTTP version \"HTTP/2\"")

shouldRetry, _ := checkRetry(context.Background(), nil, http2Error)

if !shouldRetry {
t.Error("Expected shouldRetry=true when DisableHTTPFallback=false")
}
}

func TestNonHTTP2ErrorsRetry(t *testing.T) {
opts := &Options{
Protocol: "http11",
DisableHTTPFallback: true,
Timeout: 5 * time.Second,
RetryMax: 3,
}

client, err := NewClient(opts)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

checkRetry := client.NewCheckRetryFunc()
timeoutErr := errors.New("context deadline exceeded")

shouldRetry, _ := checkRetry(context.Background(), nil, timeoutErr)

if !shouldRetry {
t.Error("Expected timeout error to still trigger retry")
}
}

func TestIsHTTP2FallbackError(t *testing.T) {
testCases := []struct {
name string
errMsg string
shouldMatch bool
}{
{
name: "Malformed HTTP/2 version",
errMsg: "malformed HTTP version \"HTTP/2\"",
shouldMatch: true,
},
{
name: "Malformed HTTP response",
errMsg: "malformed HTTP response",
shouldMatch: true,
},
{
name: "Generic error",
errMsg: "connection refused",
shouldMatch: false,
},
{
name: "Timeout error",
errMsg: "context deadline exceeded",
shouldMatch: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := errors.New(tc.errMsg)
result := isHTTP2FallbackError(err)

if result != tc.shouldMatch {
t.Errorf("Expected %v, got %v for: %s", tc.shouldMatch, result, tc.errMsg)
}
})
}
}

2 changes: 2 additions & 0 deletions common/httpx/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type Options struct {
CDNCheckClient *cdncheck.Client
Protocol Proto
Trace bool
DisableHTTPFallback bool
}

// DefaultOptions contains the default options
Expand All @@ -84,6 +85,7 @@ var DefaultOptions = Options{
VHostStripHTML: false,
VHostSimilarityRatio: 85,
DefaultUserAgent: "httpx - Open-source project (github.com/projectdiscovery/httpx)",
DisableHTTPFallback: false,
}

func (options *Options) parseCustomCookies() {
Expand Down
29 changes: 29 additions & 0 deletions common/httpx/retry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package httpx

import (
"context"
"net/http"
"strings"

"github.com/hashicorp/go-retryablehttp"
)
Comment on lines +8 to +9
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.


func isHTTP2FallbackError(err error) bool {
if err == nil {
return false
}
errorMsg := err.Error()
return strings.Contains(errorMsg, "malformed HTTP version \"HTTP/2\"") ||
strings.Contains(errorMsg, "malformed HTTP response")
}

func (c *Client) NewCheckRetryFunc() retryablehttp.CheckRetryFunc {
return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if c.Options.Protocol == "http11" && c.Options.DisableHTTPFallback {
if isHTTP2FallbackError(err) {
return false, err
}
}
return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
}
}
18 changes: 10 additions & 8 deletions runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ type Options struct {
ResultDatabaseTable string
ResultDatabaseBatchSize int
ResultDatabaseOmitRaw bool

DisableHTTPFallback bool
// Optional pre-created objects to reduce allocations
Wappalyzer *wappalyzer.Wappalyze
Networkpolicy *networkpolicy.NetworkPolicy
Expand Down Expand Up @@ -518,11 +518,11 @@ func ParseOptions() *Options {
flagSet.BoolVarP(&options.ResultDatabaseOmitRaw, "result-db-omit-raw", "rdbor", false, "omit raw request/response data from database"),
)

flagSet.CreateGroup("configs", "Configurations",
flagSet.StringVar(&cfgFile, "config", "", "path to the httpx configuration file (default $HOME/.config/httpx/config.yaml)"),
flagSet.StringSliceVarP(&options.Resolvers, "resolvers", "r", nil, "list of custom resolver (file or comma separated)", goflags.NormalizedStringSliceOptions),
flagSet.Var(&options.Allow, "allow", "allowed list of IP/CIDR's to process (file or comma separated)"),
flagSet.Var(&options.Deny, "deny", "denied list of IP/CIDR's to process (file or comma separated)"),
flagSet.CreateGroup("configs", "Configurations",
flagSet.StringVar(&cfgFile, "config", "", "path to the httpx configuration file (default $HOME/.config/httpx/config.yaml)"),
flagSet.StringSliceVarP(&options.Resolvers, "resolvers", "r", nil, "list of custom resolver (file or comma separated)", goflags.NormalizedStringSliceOptions),
flagSet.Var(&options.Allow, "allow", "allowed list of IP/CIDR's to process (file or comma separated)"),
flagSet.Var(&options.Deny, "deny", "denied list of IP/CIDR's to process (file or comma separated)"),
flagSet.StringVarP(&options.SniName, "sni-name", "sni", "", "custom TLS SNI name"),
flagSet.BoolVar(&options.RandomAgent, "random-agent", true, "enable Random User-Agent to use"),
flagSet.BoolVar(&options.AutoReferer, "auto-referer", false, "set the Referer header to the current URL"),
Expand All @@ -531,6 +531,7 @@ func ParseOptions() *Options {
flagSet.BoolVar(&options.Unsafe, "unsafe", false, "send raw requests skipping golang normalization"),
flagSet.BoolVar(&options.Resume, "resume", false, "resume scan using resume.cfg"),
flagSet.BoolVarP(&options.FollowRedirects, "follow-redirects", "fr", false, "follow http redirects"),
flagSet.BoolVarP(&options.DisableHTTPFallback, "dhf", "", false, "Disable HTTP/2 fallback on protocol errors when using HTTP/1.1 (-pr http11)"),
flagSet.IntVarP(&options.MaxRedirects, "max-redirects", "maxr", 10, "max number of redirects to follow per host"),
flagSet.BoolVarP(&options.FollowHostRedirects, "follow-host-redirects", "fhr", false, "follow redirects on the same host"),
flagSet.BoolVarP(&options.RespectHSTS, "respect-hsts", "rhsts", false, "respect HSTS response headers for redirect requests"),
Expand All @@ -546,7 +547,7 @@ func ParseOptions() *Options {
flagSet.BoolVar(&options.DisableStdin, "no-stdin", false, "Disable Stdin processing"),
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
flagSet.StringVarP(&options.SecretFile, "secret-file", "sf", "", "path to the secret file for authentication"),
)
)

flagSet.CreateGroup("debug", "Debug",
flagSet.BoolVarP(&options.HealthCheck, "hc", "health-check", false, "run diagnostic check up"),
Expand All @@ -561,7 +562,8 @@ func ParseOptions() *Options {
flagSet.IntVarP(&options.StatsInterval, "stats-interval", "si", 0, "number of seconds to wait between showing a statistics update (default: 5)"),
flagSet.BoolVarP(&options.NoColor, "no-color", "nc", false, "disable colors in cli output"),
flagSet.BoolVarP(&options.Trace, "trace", "tr", false, "trace"),
)

)

flagSet.CreateGroup("Optimizations", "Optimizations",
flagSet.BoolVarP(&options.NoFallback, "no-fallback", "nf", false, "display both probed protocol (HTTPS and HTTP)"),
Expand Down