Skip to content

Add support for duplicate custom request headers#2504

Open
ayanrajpoot10 wants to merge 2 commits into
projectdiscovery:devfrom
ayanrajpoot10:dev
Open

Add support for duplicate custom request headers#2504
ayanrajpoot10 wants to merge 2 commits into
projectdiscovery:devfrom
ayanrajpoot10:dev

Conversation

@ayanrajpoot10
Copy link
Copy Markdown
Contributor

@ayanrajpoot10 ayanrajpoot10 commented May 23, 2026

Proposed changes

This PR adds support for duplicate custom HTTP request headers in httpx, aligning its behavior with tools like curl and the HTTP specification, which allows multiple headers with the same key.

Previously, CustomHeaders used a map[string]string. When multiple headers with the same name were provided (for example, -H "X-Test: one" -H "X-Test: two"), earlier values were overwritten and only the last value was sent in the request.

This PR fixes issue #2503.

Changes:

  • Refactored CustomHeaders from map[string]string to map[string][]string, matching Go's native http.Header structure.
  • Updated httputilz.ParseRequest() to correctly preserve duplicate headers from raw requests.
  • Replaced r.Header.Set() with r.Header.Add() in SetCustomHeaders() so all header values are appended instead of overwritten.

Before

Command:

echo "https://httpbin.org/anything" | ./httpx -debug -H "X-Test: one" -H "X-Test: two"

Output:

   / /_  / /_/ /_____ | |/ /
  / __ \/ __/ __/ __ \|   /
 / / / / /_/ /_/ /_/ /   |
/_/ /_/\__/\__/ .___/_/|_|
             /_/

		projectdiscovery.io

[INF] Current httpx version v1.9.0 (latest)
[WRN] UI Dashboard is disabled, Use -dashboard option to enable
[INF] Dumped HTTP request for https://httpbin.org/anything

GET /anything HTTP/1.1
Host: httpbin.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15
Accept-Charset: utf-8
X-Test: two
Accept-Encoding: gzip

[INF] Dumped HTTP response for https://httpbin.org/anything

HTTP/1.1 200 OK
Connection: close
Content-Length: 510
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 23 May 2026 04:31:49 GMT
Server: gunicorn/19.9.0

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept-Charset": "utf-8", 
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15", 
    "X-Amzn-Trace-Id": "Root=1-6a112db4-6942045f664155291f8afada", 
    "X-Test": "two"
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "20.235.80.242", 
  "url": "https://httpbin.org/anything"
}
https://httpbin.org/anything

After

Command:

echo "https://httpbin.org/anything" | ./httpx -debug -H "X-Test: one" -H "X-Test: two"

Output:

   / /_  / /_/ /_____ | |/ /
  / __ \/ __/ __/ __ \|   /
 / / / / /_/ /_/ /_/ /   |
/_/ /_/\__/\__/ .___/_/|_|
             /_/

		projectdiscovery.io

[INF] Current httpx version v1.9.0 (latest)
[WRN] UI Dashboard is disabled, Use -dashboard option to enable
[INF] Dumped HTTP request for https://httpbin.org/anything

GET /anything HTTP/1.1
Host: httpbin.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/E7FBAF
Accept-Charset: utf-8
X-Test: one
X-Test: two
Accept-Encoding: gzip

[INF] Dumped HTTP response for https://httpbin.org/anything

HTTP/1.1 200 OK
Connection: close
Content-Length: 516
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Sat, 23 May 2026 04:29:41 GMT
Server: gunicorn/19.9.0

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept-Charset": "utf-8", 
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/E7FBAF", 
    "X-Amzn-Trace-Id": "Root=1-6a112d35-1ea397621691623b5cc23f2f", 
    "X-Test": "one,two"
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "20.235.80.242", 
  "url": "https://httpbin.org/anything"
}
https://httpbin.org/anything

Both header values are now correctly preserved and sent in the request.

Proof

  • Verified the project builds successfully with go build ./...
  • Ran the existing test suite successfully with go test ./...
  • Tested locally with duplicate -H flags and confirmed both headers are included in the final request

Checklist

  • Pull request is created against the [dev](https://github.com/projectdiscovery/httpx/tree/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
    • Improved HTTP header handling to preserve multiple values for the same header name across request parsing and custom header application.
    • Host and Cookie headers are now handled consistently when multiple header values are present, preventing overwrites and preserving all provided entries.

Review Change Stack

@auto-assign auto-assign Bot requested a review from dwisiswant0 May 23, 2026 04:50
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Walkthrough

Header handling is refactored across the codebase to support multiple values per header key. ParseRequest, HTTPX.CustomHeaders, Options, and runner initialization are all updated to use multivalue maps (map[string][]string) with append-based accumulation instead of single-value replacement.

Changes

Multivalue HTTP Header Support

Layer / File(s) Summary
Request parsing for multivalue headers
common/httputilz/httputilz.go
ParseRequest return type changes from map[string]string to map[string][]string. Header values are appended instead of overwritten, and the Host header is stored as a single-element slice to align with the new representation.
Options and cookie header handling
common/httpx/option.go
Options.CustomHeaders field changed to multivalue type. Cookie parsing logic adapted to use slice values directly instead of wrapping single strings.
HTTPX multivalue header storage and application
common/httpx/httpx.go
HTTPX.CustomHeaders field type and SetCustomHeaders method signature updated to accept and apply multivalue headers. Uses r.Header.Add for non-host headers and special-cases Host by setting r.Host directly (and adding Host header when Unsafe).
Runner integration and header accumulation
runner/runner.go
Runner New() function updated to initialize httpxOptions.CustomHeaders as multivalue map and accumulate repeated header keys via append. Import statements reordered.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I nibble headers, one becomes many,
Slices gather values, cheerful and sunny,
Append, don't clobber, each cookie and host,
Multivalue blossoms—I'll toast with a roast! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: enabling support for duplicate custom request headers by refactoring from map[string]string to map[string][]string.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@runner/runner.go`:
- Line 246: The code currently overwrites existing entries with
httpxOptions.CustomHeaders[customHeader] = []string{""}, which drops duplicate
identical -H inputs; change it to append an empty string to the existing slice
instead (e.g. httpxOptions.CustomHeaders[customHeader] =
append(httpxOptions.CustomHeaders[customHeader], "")) so repeated identical
customHeader values are preserved in httpxOptions.CustomHeaders rather than
replaced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07d4fc43-8304-40fd-870e-9a1a094e64f9

📥 Commits

Reviewing files that changed from the base of the PR and between 41e6d9b and 8eb7d85.

📒 Files selected for processing (4)
  • common/httputilz/httputilz.go
  • common/httpx/httpx.go
  • common/httpx/option.go
  • runner/runner.go

Comment thread runner/runner.go
// rawhttp skips all checks
if options.Unsafe {
httpxOptions.CustomHeaders[customHeader] = ""
httpxOptions.CustomHeaders[customHeader] = []string{""}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve repeated identical -H entries in unsafe mode.

Line 246 still overwrites prior values for the same raw header string in unsafe mode. Repeating the exact same -H input loses duplicates.

Suggested fix
-			httpxOptions.CustomHeaders[customHeader] = []string{""}
+			httpxOptions.CustomHeaders[customHeader] = append(httpxOptions.CustomHeaders[customHeader], "")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runner/runner.go` at line 246, The code currently overwrites existing entries
with httpxOptions.CustomHeaders[customHeader] = []string{""}, which drops
duplicate identical -H inputs; change it to append an empty string to the
existing slice instead (e.g. httpxOptions.CustomHeaders[customHeader] =
append(httpxOptions.CustomHeaders[customHeader], "")) so repeated identical
customHeader values are preserved in httpxOptions.CustomHeaders rather than
replaced.

Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
common/httpx/httpx.go (1)

437-454: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Coalesce canonical header keys before Del/Add to preserve case-variant duplicates.

net/http canonicalizes header keys for Header.Del/Header.Add, but SetCustomHeaders does r.Header.Del(name) per raw map key; if the input contains both X-Test and x-test, the later Del wipes the values added for the earlier one. Merge values by canonical key first, then apply once per canonical key.

Suggested fix
 import (
 	"context"
 	"crypto/tls"
 	"fmt"
 	"io"
 	"net"
 	"net/http"
+	"net/textproto"
 	"net/url"
 	"os"
 	"strconv"
 	"strings"
 	"time"
@@
 func (h *HTTPX) SetCustomHeaders(r *retryablehttp.Request, headers map[string][]string) {
-	for name, values := range headers {
-		r.Header.Del(name)
+	normalized := make(map[string][]string, len(headers))
+	for name, values := range headers {
+		canonical := textproto.CanonicalMIMEHeaderKey(name)
+		normalized[canonical] = append(normalized[canonical], values...)
+	}
+
+	for name, values := range normalized {
+		r.Header.Del(name)
 		for _, value := range values {
 			switch strings.ToLower(name) {
 			case "host":
 				r.Host = value
 				if h.Options.Unsafe {
 					r.Header.Add("Host", value)
 				}
 			case "cookie":
 				// cookies are set in the default branch, and reset during the follow redirect flow
 				fallthrough
 			default:
 				r.Header.Add(name, value)
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/httpx/httpx.go` around lines 437 - 454, The SetCustomHeaders function
currently calls r.Header.Del(name) and r.Header.Add(name, value) per raw map key
which breaks when input contains case-variant duplicates (e.g., "X-Test" and
"x-test"); modify HTTPX.SetCustomHeaders to first coalesce the incoming headers
by their canonicalized key (use http.CanonicalHeaderKey or strings.ToLower?;
prefer http.CanonicalHeaderKey) into a single map[string][]string, then iterate
that canonical-key map once: call r.Header.Del(canonicalKey) and add all merged
values (handling special cases like "Host" and "Cookie" exactly as existing
logic does) so duplicates differing only by case are preserved instead of
overwritten.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@common/httpx/httpx.go`:
- Around line 437-454: The SetCustomHeaders function currently calls
r.Header.Del(name) and r.Header.Add(name, value) per raw map key which breaks
when input contains case-variant duplicates (e.g., "X-Test" and "x-test");
modify HTTPX.SetCustomHeaders to first coalesce the incoming headers by their
canonicalized key (use http.CanonicalHeaderKey or strings.ToLower?; prefer
http.CanonicalHeaderKey) into a single map[string][]string, then iterate that
canonical-key map once: call r.Header.Del(canonicalKey) and add all merged
values (handling special cases like "Host" and "Cookie" exactly as existing
logic does) so duplicates differing only by case are preserved instead of
overwritten.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 238619ef-5db5-4303-8324-82612a847bd9

📥 Commits

Reviewing files that changed from the base of the PR and between 8eb7d85 and 3735519.

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

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.

1 participant