Compare commits

...

7 Commits

Author SHA1 Message Date
Claude Bot
a6c971c86a fix(test): add headersDistinct assertions to http2 response test
The test now verifies that the server-side Http2ServerRequest.headersDistinct
contains the client-sent x-custom header as an array, and validates all
values are arrays. headersDistinct is a server-side API (Http2ServerRequest),
so assertions are done in the createServer callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-01 09:57:31 +00:00
Claude Bot
1efb3dc9ff fix(test): add HTTP/2 variants for headersDistinct/trailersDistinct tests
Cover Http2ServerRequest.headersDistinct and trailersDistinct with two
new end-to-end tests using http2.createServer and http2.connect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-01 09:41:29 +00:00
Claude Bot
aba4d7333f fix(test): improve x-multi header assertion to handle both representations
Check for val1/val2 independently with .some(), so the assertion works
whether the values are separate array elements or comma-joined in one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-01 09:27:05 +00:00
Claude Bot
ba8e4e4887 fix(test): assert x-multi header values in headersDistinct test
Add explicit assertion that the x-multi response header appears in
headersDistinct with the expected values (val1, val2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-01 09:17:27 +00:00
Claude Bot
316ad04c12 fix(test): remove unused Bun.serve and add stderr assertions in 24268 test
Address review comments: remove dead Bun.serve block (test spawns its own
server) and assert stderr is empty for better debugging on failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-01 09:03:47 +00:00
Jarred Sumner
45062a915f Merge branch 'main' into claude/fix-issue-24268-headers-distinct 2026-03-01 00:57:32 -08:00
Claude Bot
d5a603a081 fix: implement headersDistinct and trailersDistinct on IncomingMessage
Add missing `headersDistinct` and `trailersDistinct` getters to
`IncomingMessage` (node:http) and `Http2ServerRequest` (node:http2).
These properties return an object mapping lowercased header names to
arrays of values, matching the Node.js API.

Closes #24268

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 04:39:07 +00:00
3 changed files with 312 additions and 0 deletions

View File

@@ -384,6 +384,23 @@ const IncomingMessagePrototype = {
set httpVersionMinor(value) {
// noop
},
get headersDistinct() {
const src = this.rawHeaders;
const dst = Object.create(null);
for (let i = 0; i < src.length; i += 2) {
const key = src[i].toLowerCase();
const value = src[i + 1];
if (dst[key] !== undefined) {
dst[key].push(value);
} else {
dst[key] = [value];
}
}
return dst;
},
set headersDistinct(value) {
// noop
},
get rawTrailers() {
return [];
},
@@ -396,6 +413,12 @@ const IncomingMessagePrototype = {
set trailers(value) {
// noop
},
get trailersDistinct() {
return Object.create(null);
},
set trailersDistinct(value) {
// noop
},
setTimeout(msecs, callback) {
void this.take;
const req = this[kHandle] || this[webRequestOrResponse];

View File

@@ -388,10 +388,40 @@ class Http2ServerRequest extends Readable {
return this[kTrailers];
}
get headersDistinct() {
const src = this[kRawHeaders];
const dst = Object.create(null);
for (let i = 0; i < src.length; i += 2) {
const key = src[i].toLowerCase();
const value = src[i + 1];
if (dst[key] !== undefined) {
dst[key].push(value);
} else {
dst[key] = [value];
}
}
return dst;
}
get rawTrailers() {
return this[kRawTrailers];
}
get trailersDistinct() {
const src = this[kRawTrailers];
const dst = Object.create(null);
for (let i = 0; i < src.length; i += 2) {
const key = src[i].toLowerCase();
const value = src[i + 1];
if (dst[key] !== undefined) {
dst[key].push(value);
} else {
dst[key] = [value];
}
}
return dst;
}
get httpVersionMajor() {
return 2;
}

View File

@@ -0,0 +1,259 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("request.headersDistinct returns object mapping headers to arrays of values", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require("node:http");
const server = http.createServer((req, res) => {
const hd = req.headersDistinct;
const td = req.trailersDistinct;
// headersDistinct should be an object (not undefined)
if (typeof hd !== "object" || hd === null) {
res.writeHead(500);
res.end("headersDistinct is not an object: " + typeof hd);
return;
}
// trailersDistinct should be an object (not undefined)
if (typeof td !== "object" || td === null) {
res.writeHead(500);
res.end("trailersDistinct is not an object: " + typeof td);
return;
}
// Each value should be an array
for (const [key, val] of Object.entries(hd)) {
if (!Array.isArray(val)) {
res.writeHead(500);
res.end("value for " + key + " is not an array");
return;
}
}
// host header should exist and be an array
const hostArr = hd["host"];
if (!Array.isArray(hostArr) || hostArr.length !== 1) {
res.writeHead(500);
res.end("host header incorrect: " + JSON.stringify(hostArr));
return;
}
res.writeHead(200);
res.end("ok");
});
server.listen(0, () => {
const port = server.address().port;
http.get("http://localhost:" + port, { headers: { "x-custom": "test-value" } }, (res) => {
let data = "";
res.on("data", (chunk) => data += chunk);
res.on("end", () => {
console.log(data);
server.close();
});
});
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("ok");
expect(stderr.trim()).toBe("");
expect(exitCode).toBe(0);
});
test("response.headersDistinct returns object mapping headers to arrays of values", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require("node:http");
const server = http.createServer((req, res) => {
res.setHeader("x-multi", ["val1", "val2"]);
res.writeHead(200);
res.end("hello");
});
server.listen(0, () => {
const port = server.address().port;
http.get("http://localhost:" + port, (res) => {
const hd = res.headersDistinct;
const td = res.trailersDistinct;
if (typeof hd !== "object" || hd === null) {
console.log("FAIL: headersDistinct is not an object: " + typeof hd);
server.close();
return;
}
if (typeof td !== "object" || td === null) {
console.log("FAIL: trailersDistinct is not an object: " + typeof td);
server.close();
return;
}
// Each value should be an array
for (const [key, val] of Object.entries(hd)) {
if (!Array.isArray(val)) {
console.log("FAIL: value for " + key + " is not an array");
server.close();
return;
}
}
// x-multi header should be present as an array containing both values
// (either as separate elements or comma-joined in a single element)
const xMulti = hd["x-multi"];
const hasVal1 = Array.isArray(xMulti) && xMulti.some(v => v.includes("val1"));
const hasVal2 = Array.isArray(xMulti) && xMulti.some(v => v.includes("val2"));
if (!hasVal1 || !hasVal2) {
console.log("FAIL: x-multi header incorrect: " + JSON.stringify(xMulti));
server.close();
return;
}
console.log("ok");
res.resume();
res.on("end", () => server.close());
});
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("ok");
expect(stderr.trim()).toBe("");
expect(exitCode).toBe(0);
});
test("http2 request.headersDistinct returns object mapping headers to arrays of values", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http2 = require("node:http2");
const server = http2.createServer((req, res) => {
const hd = req.headersDistinct;
const td = req.trailersDistinct;
if (typeof hd !== "object" || hd === null) {
res.writeHead(500);
res.end("headersDistinct is not an object: " + typeof hd);
return;
}
if (typeof td !== "object" || td === null) {
res.writeHead(500);
res.end("trailersDistinct is not an object: " + typeof td);
return;
}
for (const [key, val] of Object.entries(hd)) {
if (!Array.isArray(val)) {
res.writeHead(500);
res.end("value for " + key + " is not an array");
return;
}
}
res.writeHead(200);
res.end("ok");
});
server.listen(0, () => {
const port = server.address().port;
const client = http2.connect("http://localhost:" + port);
const req = client.request({ ":path": "/", "x-custom": "test-value" });
let data = "";
req.on("data", (chunk) => data += chunk);
req.on("end", () => {
console.log(data);
client.close();
server.close();
});
req.end();
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("ok");
expect(stderr.trim()).toBe("");
expect(exitCode).toBe(0);
});
test("http2 server verifies headersDistinct contains client-sent custom header", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http2 = require("node:http2");
const server = http2.createServer((req, res) => {
const hd = req.headersDistinct;
// Verify x-custom header is present as an array
const xCustom = hd && hd["x-custom"];
if (!Array.isArray(xCustom) || !xCustom.some(v => v.includes("test-value"))) {
res.writeHead(500);
res.end("FAIL: x-custom header incorrect: " + JSON.stringify(xCustom));
return;
}
// Verify all values are arrays
for (const [key, val] of Object.entries(hd)) {
if (!Array.isArray(val)) {
res.writeHead(500);
res.end("FAIL: value for " + key + " is not an array");
return;
}
}
res.writeHead(200);
res.end("ok");
});
server.listen(0, () => {
const port = server.address().port;
const client = http2.connect("http://localhost:" + port);
const req = client.request({ ":path": "/", "x-custom": "test-value" });
let data = "";
req.on("data", (chunk) => data += chunk);
req.on("end", () => {
console.log(data);
client.close();
server.close();
});
req.end();
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("ok");
expect(stderr.trim()).toBe("");
expect(exitCode).toBe(0);
});