diff --git a/src/js/node/_http_client.ts b/src/js/node/_http_client.ts index faebd51921..e46b423c4f 100644 --- a/src/js/node/_http_client.ts +++ b/src/js/node/_http_client.ts @@ -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. diff --git a/test/regression/issue/07520.test.ts b/test/regression/issue/07520.test.ts new file mode 100644 index 0000000000..45dc4725dc --- /dev/null +++ b/test/regression/issue/07520.test.ts @@ -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; +} + +function createHeaderCapturingServer(): Promise { + return new Promise(resolveSetup => { + let receivedData = ""; + let headersDone = false; + const { promise: waitForHeaders, resolve: resolveHeaders } = Promise.withResolvers(); + + 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(); + } +});