Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/js/node/_http_outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,19 @@ const OutgoingMessagePrototype = {
_closed: false,

appendHeader(name, value) {
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] == NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
validateString(name, "name");
validateHeaderValue(name, value);
var headers = (this[headersSymbol] ??= new Headers());
headers.append(name, value);
if ($isJSArray(value)) {
for (let i = 0; i < value.length; i++) {
headers.append(name, value[i]);
}
} else {
headers.append(name, value);
}
return this;
},

Expand Down Expand Up @@ -259,7 +269,14 @@ const OutgoingMessagePrototype = {
validateHeaderName(name);
validateHeaderValue(name, value);
const headers = (this[headersSymbol] ??= new Headers());
setHeader(headers, name, value);
if ($isJSArray(value)) {
if (value.length > 0) setHeader(headers, name, value[0]);
for (let i = 1; i < value.length; i++) {
headers.append(name, value[i]);
}
} else {
setHeader(headers, name, value);
}
return this;
},
setHeaders(headers) {
Expand Down
127 changes: 127 additions & 0 deletions test/js/node/http/fixtures/node-http-client-headers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import http from "node:http";

async function execute(test_name, options) {
console.log("%<test>" + test_name + "</test>");
const { promise, resolve, reject } = Promise.withResolvers();
http
.createServer(function (req, res) {
if (typeof Bun !== "undefined") {
// bun adds these headers by default
if (req.headers["user-agent"] === `Bun/${Bun.version}`) {
delete req.headers["user-agent"];
}
if (req.headers["accept"] === "*/*") {
delete req.headers["accept"];
}
}

this.close();

console.log(
JSON.stringify(
Object.fromEntries(Object.entries(req.headers).sort((a, b) => a[0].localeCompare(b[0]))),
).replaceAll('"', "'"),
);

res.writeHead(200, { "Connection": "close" });
res.end();
})
.listen(0, function () {
options = Object.assign(options, {
port: this.address().port,
path: "/",
});
const req = http.request(options);
req.end();
req.on("response", rsp => {
console.log("-> " + rsp.statusCode);
resolve();
});
});
await promise;
}

await execute("headers array in object", {
headers: {
"a": "one",
"b": ["two", "three"],
"cookie": ["four", "five", "six"],
"Host": "example.com",
},
});

await execute("multiple of same header in array", {
headers: [
["a", "one"],
["b", "two"],
["b", "three"],
["cookie", "four"],
["cookie", "five"],
["cookie", "six"],
["Host", "example.com"],
],
});

await execute("multiple of same header in array 2", {
headers: [
["a", "one"],
["b", ["two", "three"]],
["cookie", ["four", "five"]],
["cookie", "six"],
["Host", "example.com"],
],
});

await execute("multiple of same header in array 3", {
headers: [
["a", "one"],
["b", "two"],
["b", "three"],
["cookie", ["four", "five", "six"]],
["Host", "example.com"],
],
});

await execute("multiple of same header in flat array", {
headers: [
"a",
"one",
"b",
"two",
"b",
"three",
"cookie",
"four",
"cookie",
"five",
"cookie",
"six",
"Host",
"example.com",
],
});

await execute("arrays of headers in flat array", {
headers: ["a", "one", "b", ["two", "three"], "cookie", ["four", "five"], "cookie", "six", "Host", "example.com"],
});

await execute("set user agent and accept", {
headers: {
"abc": "def",
"user-agent": "my new user agent",
"accept": "text/html",
"host": "example.com",
},
});

await execute("set user agent and accept (array 1)", {
headers: [
["user-agent", "my new user agent"],
["accept", "text/html"],
["host", "example.com"],
],
});

await execute("set user agent and accept (flat array)", {
headers: ["user-agent", "my new user agent", "accept", "text/html", "host", "example.com"],
});
30 changes: 30 additions & 0 deletions test/js/node/http/node-http-client-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { bunExe } from "harness";

function toObject(str: string) {
const split = str.split("%<test>");
const result = {};
for (const line of split) {
if (!line.trim()) continue;
const [key, value] = line.split("</test>");
result[key] = value;
}
return result;
}

describe("node-http-client-headers", async () => {
const expected = Bun.spawnSync(["node", import.meta.dir + "/fixtures/node-http-client-headers.mjs"], {
stdio: ["ignore", "pipe", "pipe"],
});
const actual = Bun.spawnSync([bunExe(), import.meta.dir + "/fixtures/node-http-client-headers.mjs"], {
stdio: ["ignore", "pipe", "pipe"],
});
expect(actual.stderr.toString()).toEqual(expected.stderr.toString());
expect(actual.exitCode).toEqual(expected.exitCode);
const expected_obj = toObject(expected.stdout.toString());
const actual_obj = toObject(actual.stdout.toString());
for (const [key, value] of Object.entries(expected_obj)) {
test(key, () => {
expect(actual_obj[key]).toEqual(value);
});
}
});
76 changes: 76 additions & 0 deletions test/js/node/test/parallel/test-http-client-headers-array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

const {mustCall} = require('../common');

const assert = require('assert');
const http = require('http');

function execute(options) {
http.createServer(mustCall(function(req, res) {
const expectHeaders = {
'x-foo': 'boom',
'cookie': 'a=1; b=2; c=3',
'connection': 'keep-alive',
'host': 'example.com',
};

// no Host header when you set headers an array
if (!Array.isArray(options.headers)) {
expectHeaders.host = `localhost:${this.address().port}`;
}

// no Authorization header when you set headers an array
if (options.auth && !Array.isArray(options.headers)) {
expectHeaders.authorization =
`Basic ${Buffer.from(options.auth).toString('base64')}`;
}

if(typeof Bun !== 'undefined') {
// bun adds these headers by default
expectHeaders['user-agent'] ??= `Bun/${Bun.version}`;
expectHeaders['accept'] ??= '*/*';
}

this.close();

assert.deepStrictEqual(req.headers, expectHeaders);

res.writeHead(200, { 'Connection': 'close' });
res.end();
})).listen(0, mustCall(function() {
options = Object.assign(options, {
port: this.address().port,
path: '/'
});
const req = http.request(options);
req.end();
}));
}

// Should be the same except for implicit Host header on the first two
execute({ headers: { 'x-foo': 'boom', 'cookie': 'a=1; b=2; c=3' } });
execute({ headers: { 'x-foo': 'boom', 'cookie': [ 'a=1', 'b=2', 'c=3' ] } });
execute({ headers: [
[ 'x-foo', 'boom' ],
[ 'cookie', 'a=1; b=2; c=3' ],
[ 'Host', 'example.com' ],
] });
execute({ headers: [
[ 'x-foo', 'boom' ],
[ 'cookie', [ 'a=1', 'b=2', 'c=3' ]],
[ 'Host', 'example.com' ],
] });
execute({ headers: [
[ 'x-foo', 'boom' ], [ 'cookie', 'a=1' ],
[ 'cookie', 'b=2' ], [ 'cookie', 'c=3' ],
[ 'Host', 'example.com'],
] });

// Authorization and Host header both missing from the second
execute({ auth: 'foo:bar', headers:
{ 'x-foo': 'boom', 'cookie': 'a=1; b=2; c=3' } });
execute({ auth: 'foo:bar', headers: [
[ 'x-foo', 'boom' ], [ 'cookie', 'a=1' ],
[ 'cookie', 'b=2' ], [ 'cookie', 'c=3'],
[ 'Host', 'example.com'],
] });