Skip to content
70 changes: 69 additions & 1 deletion doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,8 @@ non-string values. However, the non-string values will be converted to strings
for network transmission. The same response object is returned to the caller,
to enable call chaining.

> To set multiple header values at once see [`response.setHeaders()`][].

```js
response.setHeader('Content-Type', 'text/html');
```
Expand Down Expand Up @@ -1987,6 +1989,57 @@ header will not yield the expected result. If progressive population of headers
is desired with potential future retrieval and modification, use
[`response.setHeader()`][] instead of [`response.writeHead()`][].

### `response.setHeaders(headers)`

<!-- YAML
added: REPLACEME
-->

* `headers` {Headers|Object|Array}
* Returns: {http.ServerResponse}

Returns the response object.

Sets multiple header values for implicit headers.
`headers` may be an instance of [`Headers`][] or `Array` where the keys
and values are in the same list.
It is _not_ a list of tuples. So, the even-numbered offsets are key values,
and the odd-numbered offsets are the associated values. The array is in the same
format as `request.rawHeaders`.

```js
const headers = new Headers({ foo: 'bar' });
response.setHeaders(headers);
```

or

```js
response.setHeaders(['foo', 'bar', 'fizz', 'buzz']);
```

or

```js
response.setHeaders({
foo: 'bar',
fizz: 'buzz',
});
```

When headers have been set with [`response.setHeaders()`][], they will be merged
with any headers passed to [`response.writeHead()`][], with the headers passed
to [`response.writeHead()`][] given precedence.

```js
// Returns content-type = text/plain
const server = http.createServer((req, res) => {
res.setHeaders(['Content-Type', 'text/html', 'X-Foo', 'bar']);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
```

### `response.setTimeout(msecs[, callback])`

<!-- YAML
Expand Down Expand Up @@ -2203,6 +2256,10 @@ response.writeEarlyHints({
<!-- YAML
added: v0.1.30
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/46109
description: Allow passing headers as Headers object.
- version: v14.14.0
pr-url: https://github.com/nodejs/node/pull/35274
description: Allow passing headers as an array.
Expand All @@ -2222,7 +2279,7 @@ changes:

* `statusCode` {number}
* `statusMessage` {string}
* `headers` {Object|Array}
* `headers` {Headers|Object|Array}
* Returns: {http.ServerResponse}

Sends a response header to the request. The status code is a 3-digit HTTP
Expand All @@ -2247,6 +2304,15 @@ response
.end(body);
```

`headers` may also be an instance of the [`Headers`][].

```js
const body = 'hello world';
response
.writeHead(200, new Headers({ foo: 'bar' }))
.end(body);
```

This method must only be called once on a message and it must
be called before [`response.end()`][] is called.

Expand Down Expand Up @@ -3760,6 +3826,7 @@ Set the maximum number of idle HTTP parsers. **Default:** `1000`.
[`Buffer.byteLength()`]: buffer.md#static-method-bufferbytelengthstring-encoding
[`Duplex`]: stream.md#class-streamduplex
[`HPE_HEADER_OVERFLOW`]: errors.md#hpe_header_overflow
[`Headers`]: globals.md#class-headers
[`TypeError`]: errors.md#class-typeerror
[`URL`]: url.md#the-whatwg-url-api
[`agent.createConnection()`]: #agentcreateconnectionoptions-callback
Expand Down Expand Up @@ -3803,6 +3870,7 @@ Set the maximum number of idle HTTP parsers. **Default:** `1000`.
[`response.end()`]: #responseenddata-encoding-callback
[`response.getHeader()`]: #responsegetheadername
[`response.setHeader()`]: #responsesetheadername-value
[`response.setHeaders()`]: #responsesetheadersheaders
[`response.socket`]: #responsesocket
[`response.writableEnded`]: #responsewritableended
[`response.writableFinished`]: #responsewritablefinished
Expand Down
36 changes: 36 additions & 0 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
debug = fn;
});

const { Headers } = require('internal/deps/undici/undici');

const HIGH_WATER_MARK = getDefaultHighWaterMark();

const kCorked = Symbol('corked');
Expand Down Expand Up @@ -673,6 +675,40 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
return this;
};

OutgoingMessage.prototype.setHeaders = function setHeaders(headers) {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('set');
}
let k;

if (headers instanceof Headers) {
const keys = [...headers.keys()];
for (let i = 0; i < keys.length; i++) {
k = keys[i];
if (k) this.setHeader(k, headers.get(k));
}
} else if (ArrayIsArray(headers)) {
if (headers.length % 2 !== 0) {
throw new ERR_INVALID_ARG_VALUE('headers', headers);
}

for (let n = 0; n < headers.length; n += 2) {
k = headers[n + 0];
if (k) this.setHeader(k, headers[n + 1]);
}
} else if (headers) {
const keys = ObjectKeys(headers);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0; i < keys.length; i++) {
k = keys[i];
if (k) this.setHeader(k, headers[k]);
}
}

return this;
};

OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('append');
Expand Down
29 changes: 5 additions & 24 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
'use strict';

const {
ArrayIsArray,
Error,
MathMin,
ObjectKeys,
ObjectFromEntries,
ObjectSetPrototypeOf,
RegExpPrototypeExec,
ReflectApply,
Expand Down Expand Up @@ -76,7 +76,6 @@ const {
ERR_HTTP_INVALID_STATUS_CODE,
ERR_HTTP_SOCKET_ENCODING,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_CHAR
} = codes;
const {
Expand All @@ -90,6 +89,7 @@ const { setInterval, clearInterval } = require('timers');
let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
debug = fn;
});
const { Headers } = require('internal/deps/undici/undici');

const dc = require('diagnostics_channel');
const onRequestStartChannel = dc.channel('http.server.request.start');
Expand Down Expand Up @@ -359,30 +359,11 @@ function writeHead(statusCode, reason, obj) {
let headers;
if (this[kOutHeaders]) {
// Slow-case: when progressive API and header fields are passed.
let k;
if (ArrayIsArray(obj)) {
if (obj.length % 2 !== 0) {
throw new ERR_INVALID_ARG_VALUE('headers', obj);
}

for (let n = 0; n < obj.length; n += 2) {
k = obj[n + 0];
if (k) this.setHeader(k, obj[n + 1]);
}
} else if (obj) {
const keys = ObjectKeys(obj);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0; i < keys.length; i++) {
k = keys[i];
if (k) this.setHeader(k, obj[k]);
}
}
if (k === undefined && this._header) {
throw new ERR_HTTP_HEADERS_SENT('render');
}
this.setHeaders(obj);
// Only progressive api is used
headers = this[kOutHeaders];
} else if (obj instanceof Headers) {
headers = ObjectFromEntries(obj.entries());
} else {
// Only writeHead() called
headers = obj;
Expand Down
89 changes: 89 additions & 0 deletions test/parallel/test-http-response-setheaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');


{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
res.setHeaders(['foo', '1', 'bar', '2' ]);
res.writeHead(200);
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port, headers: [] }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
res.setHeaders({
foo: '1',
bar: '2'
});
res.writeHead(200);
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
res.writeHead(200); // headers already sent
assert.throws(() => {
res.setHeaders({
foo: 'bar',
});
}, {
code: 'ERR_HTTP_HEADERS_SENT'
});
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.foo, undefined);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new globalThis.Headers({ foo: '1', bar: '2' });
res.setHeaders(headers);
res.writeHead(200);
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
35 changes: 35 additions & 0 deletions test/parallel/test-http-write-head-2.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,38 @@ const http = require('http');
}));
}));
}

{
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, new globalThis.Headers({ foo: 'bar' }));
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers.foo, 'bar');
assert.strictEqual(res.statusCode, 200);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
}));
}));
}

{
const server = http.createServer(common.mustCall((req, res) => {
res.setHeader('test', '1');
res.writeHead(200, new globalThis.Headers({ test: '2', test2: '2' }));
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers.test, '2');
assert.strictEqual(res.headers.test2, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
}));
}));
}