mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
@@ -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.
|
||||
|
||||
174
test/regression/issue/07520.test.ts
Normal file
174
test/regression/issue/07520.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user