Skip to content

fix(fetch): forward request headers to the outgoing HTTP request - #270

Open
Sonic-Y3k wants to merge 2 commits into
gost-dom:mainfrom
Sonic-Y3k:fix/fetch-forward-request-headers
Open

fix(fetch): forward request headers to the outgoing HTTP request#270
Sonic-Y3k wants to merge 2 commits into
gost-dom:mainfrom
Sonic-Y3k:fix/fetch-forward-request-headers

Conversation

@Sonic-Y3k

Copy link
Copy Markdown

Request.createHttpReq builds the *http.Request but never copies the request's Headers onto it. As a result, any header set via fetch's RequestInit (Content-Type, Authorization, custom API headers, …) is silently dropped, and servers that require those headers reject the request.

This forwards the request headers to the outgoing request.

Testing: added TestFetchForwardsHeaders; go test ./internal/fetch/ passes.


AI disclosure: This change was developed with the help of an AI coding assistant. I've reviewed and tested it myself; it follows the existing conventions and the full test suite (main module, v8engine, sobekengine) passes locally.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This pull request updates fetch request handling to forward stored headers onto the outgoing HTTP request and to normalize appended header names before forbidden-header checks. It also adds tests that verify custom headers are forwarded and Cookie/Set-Cookie headers are not included in the outgoing request.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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
Title check ✅ Passed The title clearly matches the main change: forwarding fetch request headers to the outgoing HTTP request.
Description check ✅ Passed The description accurately explains the header-forwarding bug, the fix, and the added test.
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.

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.

Request.createHttpReq built the *http.Request but never copied the request's
Headers onto it, so any header set via fetch's RequestInit (Content-Type,
Authorization, custom API headers, ...) was silently dropped. Servers that
require those headers would reject the request.
@Sonic-Y3k
Sonic-Y3k force-pushed the fix/fetch-forward-request-headers branch from abfda9a to f33b56b Compare June 13, 2026 14:13
@stroiman

Copy link
Copy Markdown
Member

Lol, I'm surprised this was never implemented, as I could remember spending some time dealing with headers; but that was apparently only for responses :D

Comment thread internal/fetch/fetch.go Outdated
// Forward the request headers to the outgoing HTTP request. Without this,
// headers set via fetch's RequestInit (e.g. Content-Type, Authorization, or
// custom API headers) were silently dropped, causing servers that require
// them to reject the request.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is one of the things I dislike about AI-generated code, excessive comments. This comments tells nothing that isn't immediately readable from from code.

Same goes for function documentation, doesn't IHMO add anything.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, I do agree with this. Will remove the doc comment.

Comment thread internal/fetch/fetch.go
// headers set via fetch's RequestInit (e.g. Content-Type, Authorization, or
// custom API headers) were silently dropped, causing servers that require
// them to reject the request.
for name, value := range r.Headers.All() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking at Headers.All() there's some special handling of Set-Cookie. And since headers were only implemented (to my surprise) for responses, I'm unsure if cookies should have special handling in request headers.

Actual handling of cookies in real requests is handled by Go's http module using a CookieJar.

And that makes me think that perhaps there should be a dedicated test of how adding Set-Cookie headers affects the actual HTTP request, not the returned *http.Request value, but the resulting request sent from the http.Client to the http.Roundtripper.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah... this turned up a bug. Headers.Append checked the forbidden-header list before lower-casing the name, and that list is lower-cased, so a capitalised Cookie/Set-Cookie bypassed the filter and got forwarded. I made Append normalise the name first (forbidden header names match case-insensitively per the Fetch spec). cookie/set-cookie are already in invalidRequestHeaders, and real request cookies go through the http.Client cookie jar, so the forwarding loop needs no Set-Cookie handling.

Added TestFetchDoesNotForwardCookieHeaders to cover it. On testing the actual sent request: TestFetchForwardsHeaders already asserts against the *http.Request the recorder captures inside the handler - i.e. after the client -> round-tripper, so it's the resulting request, not the pre-flight value.

Comment thread internal/fetch/request_headers_test.go Outdated
fetch.WithMethod("POST"),
fetch.WithHeaders([][2]types.ByteString{
{"Content-Type", "application/json+protobuf"},
{"X-Goog-Api-Key", "secret-key"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

very nitpicky, but I'd use something more anonymous, like X-Example-Header - that something that smells like a specific service

@Sonic-Y3k Sonic-Y3k Jun 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not nitpicky at all ;-) switched to X-Example-Header (and swapped application/json+protobuf for a plain application/json while here)

Match forbidden request header names case-insensitively so Cookie and
Set-Cookie set on a fetch Request are no longer forwarded to the outgoing
request; real request cookies are managed by the http.Client cookie jar.

Also drop the redundant code comments flagged in review and use a
non-service-specific header name in the test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 307bd1d3-cc87-47aa-876f-ef9fc27495d6

📥 Commits

Reviewing files that changed from the base of the PR and between abfda9a and a45a887.

📒 Files selected for processing (3)
  • internal/fetch/fetch.go
  • internal/fetch/headers.go
  • internal/fetch/request_headers_test.go

Comment thread internal/fetch/headers.go
Comment on lines 44 to +47
if slices.Index(h.invalidHeaders, name) != -1 {
return
}
h.headers = insertSorted(h.headers, Header{key: name.ToLower(), val: val}, compareHeaders)
h.headers = insertSorted(h.headers, Header{key: name, val: val}, compareHeaders)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Handle forbidden header prefixes here, not just exact names.

name = name.ToLower() fixes case-insensitive exact matches, but slices.Index(h.invalidHeaders, name) still treats the list as exact names only. That means Sec-Fetch-Site/Sec-CH-UA and Proxy-Authorization will bypass this check because "sec-fetch-site" and "proxy-authorization" do not equal the sentinel entries "sec-"/"proxy-" shown in invalidRequestHeaders below. With Request.createHttpReq() now forwarding every stored header, those forbidden prefixes will still leak onto the outbound request.

Suggested fix
 func (h *Headers) Append(name, val types.ByteString) {
 	// Forbidden header names are matched case-insensitively, so normalise the
 	// name before both the check and insertion. Keeping the list sorted is
 	// imperative for correct iteration behaviour.
 	name = name.ToLower()
-	if slices.Index(h.invalidHeaders, name) != -1 {
+	if isForbiddenHeader(name, h.invalidHeaders) {
 		return
 	}
 	h.headers = insertSorted(h.headers, Header{key: name, val: val}, compareHeaders)
 }
+
+func isForbiddenHeader(name types.ByteString, invalid []types.ByteString) bool {
+	if slices.Index(invalid, name) != -1 {
+		return true
+	}
+	s := string(name)
+	return strings.HasPrefix(s, "proxy-") || strings.HasPrefix(s, "sec-")
+}
📝 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
if slices.Index(h.invalidHeaders, name) != -1 {
return
}
h.headers = insertSorted(h.headers, Header{key: name.ToLower(), val: val}, compareHeaders)
h.headers = insertSorted(h.headers, Header{key: name, val: val}, compareHeaders)
if isForbiddenHeader(name, h.invalidHeaders) {
return
}
h.headers = insertSorted(h.headers, Header{key: name, val: val}, compareHeaders)
}
func isForbiddenHeader(name types.ByteString, invalid []types.ByteString) bool {
if slices.Index(invalid, name) != -1 {
return true
}
s := string(name)
return strings.HasPrefix(s, "proxy-") || strings.HasPrefix(s, "sec-")
}

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