diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 4fbc573d34..f3bf8d1f97 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1860,7 +1860,7 @@ bool WebCore__FetchHeaders__fastHas_(WebCore::FetchHeaders* arg0, unsigned char void WebCore__FetchHeaders__copyTo(WebCore::FetchHeaders* headers, StringPointer* names, StringPointer* values, unsigned char* buf) { - auto iter = headers->createIterator(); + auto iter = headers->createIterator(false); unsigned int i = 0; for (auto pair = iter.next(); pair; pair = iter.next()) { diff --git a/test/js/web/fetch/headers-case.test.ts b/test/js/web/fetch/headers-case.test.ts index c63c985eee..c550a0c24c 100644 --- a/test/js/web/fetch/headers-case.test.ts +++ b/test/js/web/fetch/headers-case.test.ts @@ -1,26 +1,149 @@ "use strict"; import { expect, test } from "bun:test"; -import { once } from "node:events"; -import { createServer } from "node:http"; +import { createServer as createTcpServer } from "node:net"; -test.todo("Headers retain keys case-sensitive", async () => { - await using server = createServer((req, res) => { - expect(req.rawHeaders.includes("Content-Type")).toBe(true); +// Test that fetch sends headers with proper case on the wire. +// We use a raw TCP server instead of an HTTP server because uWebSockets +// lowercases headers when parsing, which would make the test fail even +// when the client sends correct casing. +test("Headers retain keys case-sensitive on the wire", async () => { + let receivedData = ""; + let resolveDataReceived: () => void; + let headersDone = false; + const dataReceived = new Promise(resolve => { + resolveDataReceived = resolve; + }); - res.end(); - }).listen(0); + const server = createTcpServer(socket => { + socket.on("data", data => { + if (headersDone) return; + receivedData += data.toString(); + // Wait for complete headers (ending with \r\n\r\n) + if (receivedData.includes("\r\n\r\n")) { + headersDone = true; + // Send a minimal HTTP response + socket.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + socket.end(); + resolveDataReceived(); + } + }); + }); - await once(server, "listening"); + server.listen(0); + await new Promise(resolve => server.once("listening", resolve)); + const port = (server.address() as { port: number }).port; - const url = `http://localhost:${server.address().port}`; - for (const headers of [ - new Headers([["Content-Type", "text/plain"]]), - { "Content-Type": "text/plain" }, - [["Content-Type", "text/plain"]], - ]) { - await fetch(url, { headers }); + try { + // Make a fetch request with various headers + await fetch(`http://localhost:${port}/`, { + headers: { + "Content-Type": "text/plain", + "X-Custom-Header": "test-value", + Authorization: "Bearer token123", + }, + }); + + await dataReceived; + + // Verify the headers are sent with correct casing + expect(receivedData).toInclude("Content-Type: text/plain"); + expect(receivedData).toInclude("X-Custom-Header: test-value"); + expect(receivedData).toInclude("Authorization: Bearer token123"); + + // Make sure they're NOT lowercased + expect(receivedData).not.toInclude("content-type:"); + expect(receivedData).not.toInclude("x-custom-header:"); + expect(receivedData).not.toInclude("authorization:"); + } finally { + server.close(); + } +}); + +// Test with Headers object +test("Headers object retains case on the wire", async () => { + let receivedData = ""; + let resolveDataReceived: () => void; + let headersDone = false; + const dataReceived = new Promise(resolve => { + resolveDataReceived = resolve; + }); + + const server = createTcpServer(socket => { + socket.on("data", data => { + if (headersDone) return; + receivedData += data.toString(); + // Wait for complete headers (ending with \r\n\r\n) + if (receivedData.includes("\r\n\r\n")) { + headersDone = true; + socket.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + socket.end(); + resolveDataReceived(); + } + }); + }); + + server.listen(0); + await new Promise(resolve => server.once("listening", resolve)); + const port = (server.address() as { port: number }).port; + + try { + const headers = new Headers([ + ["Content-Type", "application/json"], + ["X-Request-ID", "12345"], + ]); + + await fetch(`http://localhost:${port}/`, { headers }); + + await dataReceived; + + // Verify headers are sent with correct casing + expect(receivedData).toInclude("Content-Type: application/json"); + expect(receivedData).toInclude("X-Request-ID: 12345"); + } finally { + server.close(); + } +}); + +// Test with Request object +test("Request headers retain case on the wire", async () => { + let receivedData = ""; + let resolveDataReceived: () => void; + let headersDone = false; + const dataReceived = new Promise(resolve => { + resolveDataReceived = resolve; + }); + + const server = createTcpServer(socket => { + socket.on("data", data => { + if (headersDone) return; + receivedData += data.toString(); + // Wait for complete headers (ending with \r\n\r\n) + if (receivedData.includes("\r\n\r\n")) { + headersDone = true; + socket.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + socket.end(); + resolveDataReceived(); + } + }); + }); + + server.listen(0); + await new Promise(resolve => server.once("listening", resolve)); + const port = (server.address() as { port: number }).port; + + try { + // see https://github.com/nodejs/undici/pull/3183 + const request = new Request(`http://localhost:${port}/`, { + headers: [["Content-Type", "text/plain"]], + }); + + await fetch(request, { method: "GET" }); + + await dataReceived; + + expect(receivedData).toInclude("Content-Type: text/plain"); + } finally { + server.close(); } - // see https://github.com/nodejs/undici/pull/3183 - await fetch(new Request(url, { headers: [["Content-Type", "text/plain"]] }), { method: "GET" }); });