fix: respect user-provided Connection header in fetch() requests (#21049)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jarred-sumner-bot <220441119+jarred-sumner-bot@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
jarred-sumner-bot
2025-07-14 20:53:46 -07:00
committed by GitHub
parent 7f29446d9b
commit 5fe0c034e2
3 changed files with 100 additions and 3 deletions

View File

@@ -542,6 +542,7 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
var override_accept_encoding = false;
var override_accept_header = false;
var override_host_header = false;
var override_connection_header = false;
var override_user_agent = false;
var add_transfer_encoding = true;
var original_content_length: ?string = null;
@@ -560,8 +561,10 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
continue;
},
hashHeaderConst("Connection") => {
if (!this.flags.disable_keepalive) {
continue;
override_connection_header = true;
const connection_value = this.headerStr(header_values[i]);
if (std.ascii.eqlIgnoreCase(connection_value, "close")) {
this.flags.disable_keepalive = true;
}
},
hashHeaderConst("if-modified-since") => {
@@ -609,7 +612,7 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
header_count += 1;
}
if (!this.flags.disable_keepalive) {
if (!override_connection_header and !this.flags.disable_keepalive) {
request_headers_buf[header_count] = connection_header;
header_count += 1;
}

View File

@@ -0,0 +1,93 @@
import { serve } from "bun";
import { describe, expect, it, test } from "bun:test";
describe("fetch Connection header", () => {
// Helper function to capture headers from a request
const captureHeadersFromRequest = async (fetchOptions: RequestInit): Promise<Record<string, string>> => {
return new Promise((resolve, reject) => {
// Create a temporary server to capture headers
const tempServer = serve({
port: 0,
fetch(req) {
const capturedHeaders: Record<string, string> = {};
for (const [name, value] of req.headers.entries()) {
capturedHeaders[name.toLowerCase()] = value;
}
tempServer.stop();
resolve(capturedHeaders);
return new Response("OK");
},
});
const tempPort = tempServer.port;
const url = `http://localhost:${tempPort}/test`;
// Make the request to temp server
fetch(url, fetchOptions)
.then(response => {
if (response.status !== 200) {
tempServer.stop();
reject(new Error(`Expected status 200, got ${response.status}`));
}
})
.catch(error => {
tempServer.stop();
reject(error);
});
});
};
test.each([
["close", "close"],
["keep-alive", "keep-alive"],
["upgrade", "upgrade"],
["Upgrade", "Upgrade"], // Test case preservation
])("should respect Connection: %s header", async (inputValue, expectedValue) => {
const headers = await captureHeadersFromRequest({
headers: { Connection: inputValue },
});
expect(headers.connection).toBe(expectedValue);
});
test.each([
["connection", "close"],
["Connection", "close"],
["CONNECTION", "close"],
])("should respect case-insensitive header name: %s", async (headerName, expectedValue) => {
const headers = await captureHeadersFromRequest({
headers: { [headerName]: expectedValue },
});
expect(headers.connection).toBe(expectedValue);
});
it("should respect Connection header in Request object", async () => {
const headers = await captureHeadersFromRequest({
headers: { Connection: "close" },
});
expect(headers.connection).toBe("close");
});
it("should default to keep-alive when no Connection header provided", async () => {
const headers = await captureHeadersFromRequest({});
expect(headers.connection).toBe("keep-alive");
});
it("should handle multiple headers including Connection", async () => {
const headers = await captureHeadersFromRequest({
headers: {
"accept": "application/json",
"accept-encoding": "gzip, deflate",
"accept-language": "en-US",
"connection": "close",
"user-agent": "test-agent",
"x-test-header": "test-value",
},
});
expect(headers.connection).toBe("close");
expect(headers.accept).toBe("application/json");
expect(headers["x-test-header"]).toBe("test-value");
});
});

View File

@@ -519,6 +519,7 @@ test/js/web/fetch/client-fetch.test.ts
test/js/web/fetch/content-length.test.js
test/js/web/fetch/cookies.test.ts
test/js/web/fetch/fetch-args.test.ts
test/js/web/fetch/fetch-connection-header.test.ts
test/js/web/fetch/fetch-gzip.test.ts
test/js/web/fetch/fetch-preconnect.test.ts
test/js/web/fetch/fetch-redirect.test.ts