mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(fetch): preserve header case when sending HTTP requests (#26425)
## Summary - Fixes #26422 - Preserve HTTP header case when sending requests (e.g., `Content-Type` instead of `content-type`) - HTTP headers are technically case-insensitive per RFC 7230, but many APIs expect specific casing ## Test plan - [x] Added tests that verify headers are sent with proper case on the wire - [x] Tests use raw TCP sockets to capture actual HTTP wire format - [x] Tests fail with system Bun (lowercase headers), pass with fixed build 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
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;
|
unsigned int i = 0;
|
||||||
|
|
||||||
for (auto pair = iter.next(); pair; pair = iter.next()) {
|
for (auto pair = iter.next(); pair; pair = iter.next()) {
|
||||||
|
|||||||
@@ -1,26 +1,149 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import { once } from "node:events";
|
import { createServer as createTcpServer } from "node:net";
|
||||||
import { createServer } from "node:http";
|
|
||||||
|
|
||||||
test.todo("Headers retain keys case-sensitive", async () => {
|
// Test that fetch sends headers with proper case on the wire.
|
||||||
await using server = createServer((req, res) => {
|
// We use a raw TCP server instead of an HTTP server because uWebSockets
|
||||||
expect(req.rawHeaders.includes("Content-Type")).toBe(true);
|
// 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<void>(resolve => {
|
||||||
|
resolveDataReceived = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
res.end();
|
const server = createTcpServer(socket => {
|
||||||
}).listen(0);
|
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<void>(resolve => server.once("listening", resolve));
|
||||||
|
const port = (server.address() as { port: number }).port;
|
||||||
|
|
||||||
const url = `http://localhost:${server.address().port}`;
|
try {
|
||||||
for (const headers of [
|
// Make a fetch request with various headers
|
||||||
new Headers([["Content-Type", "text/plain"]]),
|
await fetch(`http://localhost:${port}/`, {
|
||||||
{ "Content-Type": "text/plain" },
|
headers: {
|
||||||
[["Content-Type", "text/plain"]],
|
"Content-Type": "text/plain",
|
||||||
]) {
|
"X-Custom-Header": "test-value",
|
||||||
await fetch(url, { headers });
|
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<void>(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<void>(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<void>(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<void>(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" });
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user