fix(http): preserve header case for node:http requests

Fixes #7520

The node:http module was lowercasing all HTTP header names when sending
requests, while Node.js preserves the original case. This was caused by
passing `this.getHeaders()` (which returns a plain object with lowercased
keys via `headers.toJSON()`) instead of the Headers object directly.

This commit changes `_http_client.ts` to pass `this[headersSymbol]` (the
raw Headers object) to the fetch call, which allows the existing header
case preservation logic (added in #26425) to work correctly for node:http
requests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-27 08:47:32 +00:00
parent ba426210c2
commit 2a8d1ab983
2 changed files with 177 additions and 1 deletions

View File

@@ -51,6 +51,7 @@ const {
reqSymbol,
callCloseCallback,
emitCloseNTAndComplete,
headersSymbol,
} = require("internal/http");
const { globalAgent } = require("node:_http_agent");
@@ -312,7 +313,8 @@ function ClientRequest(input, options, cb) {
const fetchOptions: any = {
method,
headers: this.getHeaders(),
// Use Headers object directly to preserve original header case (not getHeaders() which lowercases keys)
headers: this[headersSymbol],
redirect: "manual",
signal: this[kAbortController]?.signal,
// Timeouts are handled via this.setTimeout.

View File

@@ -0,0 +1,174 @@
import { expect, test } from "bun:test";
import http from "node:http";
import { createServer as createTcpServer, type Server } from "node:net";
// Issue #7520: node:http module lowercases HTTP header names when sending requests
// Node.js preserves the original case of header names, but Bun was lowercasing them.
// This test verifies that header names are preserved in their original case.
interface HeaderCapturingServer {
server: Server;
port: number;
getReceivedData: () => string;
waitForHeaders: Promise<void>;
}
function createHeaderCapturingServer(): Promise<HeaderCapturingServer> {
return new Promise(resolveSetup => {
let receivedData = "";
let headersDone = false;
const { promise: waitForHeaders, resolve: resolveHeaders } = Promise.withResolvers<void>();
const server = createTcpServer(socket => {
socket.on("data", data => {
if (headersDone) return;
receivedData += data.toString();
if (receivedData.includes("\r\n\r\n")) {
headersDone = true;
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
socket.end();
resolveHeaders();
}
});
});
server.listen(0, () => {
const port = (server.address() as { port: number }).port;
resolveSetup({
server,
port,
getReceivedData: () => receivedData,
waitForHeaders,
});
});
});
}
test("node:http request preserves header case for custom headers", async () => {
const { server, port, getReceivedData, waitForHeaders } = await createHeaderCapturingServer();
try {
const req = http.request(
{
hostname: "localhost",
port: port,
path: "/",
method: "GET",
headers: {
"X-Custom-Header": "test-value",
"X-Another-Header": "another-value",
Authorization: "Bearer token123",
"Content-Type": "application/json",
},
},
(res: any) => {
res.on("data", () => {});
res.on("end", () => {});
},
);
req.on("error", () => {});
req.end();
await waitForHeaders;
const receivedData = getReceivedData();
// Verify headers are sent with correct casing (not lowercased)
expect(receivedData).toInclude("X-Custom-Header: test-value");
expect(receivedData).toInclude("X-Another-Header: another-value");
expect(receivedData).toInclude("Authorization: Bearer token123");
expect(receivedData).toInclude("Content-Type: application/json");
// Make sure they're NOT lowercased
expect(receivedData).not.toInclude("x-custom-header:");
expect(receivedData).not.toInclude("x-another-header:");
expect(receivedData).not.toInclude("authorization:");
expect(receivedData).not.toInclude("content-type:");
} finally {
server.close();
}
});
test("node:http request preserves header case for POST requests", async () => {
const { server, port, getReceivedData, waitForHeaders } = await createHeaderCapturingServer();
try {
const req = http.request(
{
hostname: "localhost",
port: port,
path: "/",
method: "POST",
headers: {
"X-Request-ID": "12345",
"Content-Type": "application/json",
},
},
(res: any) => {
res.on("data", () => {});
res.on("end", () => {});
},
);
req.on("error", () => {});
req.write('{"test": true}');
req.end();
await waitForHeaders;
const receivedData = getReceivedData();
// Verify headers are sent with correct casing
expect(receivedData).toInclude("X-Request-ID: 12345");
expect(receivedData).toInclude("Content-Type: application/json");
// Make sure they're NOT lowercased
expect(receivedData).not.toInclude("x-request-id:");
expect(receivedData).not.toInclude("content-type:");
} finally {
server.close();
}
});
test("node:http request preserves header case when using setHeader()", async () => {
const { server, port, getReceivedData, waitForHeaders } = await createHeaderCapturingServer();
try {
const req = http.request({
hostname: "localhost",
port: port,
path: "/",
method: "GET",
});
// Set headers using setHeader() method
req.setHeader("X-Custom-Header", "value1");
req.setHeader("X-Another-Custom-Header", "value2");
req.setHeader("Authorization", "Bearer mytoken");
req.on("response", (res: any) => {
res.on("data", () => {});
res.on("end", () => {});
});
req.on("error", () => {});
req.end();
await waitForHeaders;
const receivedData = getReceivedData();
// Verify headers are sent with correct casing
expect(receivedData).toInclude("X-Custom-Header: value1");
expect(receivedData).toInclude("X-Another-Custom-Header: value2");
expect(receivedData).toInclude("Authorization: Bearer mytoken");
// Make sure they're NOT lowercased
expect(receivedData).not.toInclude("x-custom-header:");
expect(receivedData).not.toInclude("x-another-custom-header:");
expect(receivedData).not.toInclude("authorization:");
} finally {
server.close();
}
});