diff --git a/packages/bun-uws/src/HttpParser.h b/packages/bun-uws/src/HttpParser.h index d450d45e5f..dcbc994173 100644 --- a/packages/bun-uws/src/HttpParser.h +++ b/packages/bun-uws/src/HttpParser.h @@ -504,6 +504,11 @@ namespace uWS return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) || c == '-'; } + /* RFC 9110 Section 5.5: optional whitespace (OWS) is SP or HTAB */ + static inline bool isHTTPHeaderValueWhitespace(unsigned char c) { + return c == ' ' || c == '\t'; + } + static inline int isHTTPorHTTPSPrefixForProxies(char *data, char *end) { // We can check 8 because: // 1. If it's "http://" that's 7 bytes, and it's supposed to at least have a trailing slash. @@ -775,13 +780,13 @@ namespace uWS /* Store this header, it is valid */ headers->value = std::string_view(preliminaryValue, (size_t) (postPaddedBuffer - preliminaryValue)); postPaddedBuffer += 2; - /* Trim trailing whitespace (SP, HTAB) */ - while (headers->value.length() && headers->value.back() < 33) { + /* Trim trailing whitespace (SP, HTAB) per RFC 9110 Section 5.5 */ + while (headers->value.length() && isHTTPHeaderValueWhitespace(headers->value.back())) { headers->value.remove_suffix(1); } - /* Trim initial whitespace (SP, HTAB) */ - while (headers->value.length() && headers->value.front() < 33) { + /* Trim initial whitespace (SP, HTAB) per RFC 9110 Section 5.5 */ + while (headers->value.length() && isHTTPHeaderValueWhitespace(headers->value.front())) { headers->value.remove_prefix(1); } diff --git a/test/regression/issue/08893.test.ts b/test/regression/issue/08893.test.ts new file mode 100644 index 0000000000..1ec88d5592 --- /dev/null +++ b/test/regression/issue/08893.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from "bun:test"; +import net from "net"; + +// Regression test for https://github.com/oven-sh/bun/issues/8893 +// Bytes >= 0x80 in HTTP header values were incorrectly stripped because +// the whitespace trimming in HttpParser.h compared signed chars against 33. +// On platforms where char is signed (x86_64), bytes 0x80-0xFF are negative +// and thus < 33, causing them to be trimmed as if they were whitespace. + +test("header values preserve bytes >= 0x80", async () => { + let receivedValue: string | null = null; + + await using server = Bun.serve({ + port: 0, + fetch(req) { + receivedValue = req.headers.get("x-test"); + return new Response("OK"); + }, + }); + + const client = net.connect(server.port, "127.0.0.1"); + + // Send a raw HTTP request with 0xFF bytes surrounding the header value + const request = Buffer.concat([ + Buffer.from("GET / HTTP/1.1\r\nHost: localhost\r\nX-Test: "), + Buffer.from([0xff]), + Buffer.from("value"), + Buffer.from([0xff]), + Buffer.from("\r\n\r\n"), + ]); + + await new Promise((resolve, reject) => { + client.on("error", reject); + client.on("data", data => { + const response = data.toString(); + expect(response).toContain("HTTP/1.1 200"); + // The header value should preserve the 0xFF bytes — not strip them. + // 0xFF as a Latin-1 byte becomes U+00FF (ÿ) in the JS string. + expect(receivedValue).not.toBeNull(); + expect(receivedValue!.length).toBe(7); + expect(receivedValue!.charCodeAt(0)).toBe(0xff); + expect(receivedValue!.charCodeAt(6)).toBe(0xff); + client.end(); + resolve(); + }); + client.write(request); + }); +}); + +test("header values still trim actual whitespace (SP, HTAB)", async () => { + let receivedValue: string | null = null; + + await using server = Bun.serve({ + port: 0, + fetch(req) { + receivedValue = req.headers.get("x-test"); + return new Response("OK"); + }, + }); + + const client = net.connect(server.port, "127.0.0.1"); + + // Send a raw HTTP request with spaces and tabs surrounding the header value + const request = Buffer.from("GET / HTTP/1.1\r\nHost: localhost\r\nX-Test: \t value \t \r\n\r\n"); + + await new Promise((resolve, reject) => { + client.on("error", reject); + client.on("data", data => { + const response = data.toString(); + expect(response).toContain("HTTP/1.1 200"); + expect(receivedValue).toBe("value"); + client.end(); + resolve(); + }); + client.write(request); + }); +});