Skip to content

Commit 153d410

Browse files
committed
http: add separateHeadersValues option to request and createServer
1 parent ad747a8 commit 153d410

File tree

8 files changed

+528
-27
lines changed

8 files changed

+528
-27
lines changed

doc/api/http.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ changes:
179179
**Default:** `'lifo'`.
180180
* `timeout` {number} Socket timeout in milliseconds.
181181
This will set the timeout when the socket is created.
182+
* `separateHeadersValues` {boolean} If set to `true`, the request and the
183+
response headers will never be squashed.
184+
All request headers will be sent multiple times if the header
185+
value was set to an array or set multiple times.
186+
All response headers will always be returned as a string if the
187+
header has been received once or as an array of strings if the header has
188+
been received multiple times.
189+
In both cases headers matching is case insensitive.
182190

183191
`options` in [`socket.connect()`][] are also supported.
184192

@@ -983,6 +991,10 @@ or
983991
request.setHeader('Cookie', ['type=ninja', 'language=javascript']);
984992
```
985993

994+
If `separateHeadersValues` was set to `true` when the request was created, then
995+
using this method will always append a new value to the list of values for this
996+
header.
997+
986998
### `request.setNoDelay([noDelay])`
987999

9881000
<!-- YAML
@@ -1629,6 +1641,10 @@ Reads out a header that's already been queued but not sent to the client.
16291641
The name is case-insensitive. The type of the return value depends
16301642
on the arguments provided to [`response.setHeader()`][].
16311643

1644+
If `separateHeadersValues` was set to `true` when the server was created, then
1645+
the return value is always a string if the header has been set once or
1646+
an array of strings if the header has been set multiple times.
1647+
16321648
```js
16331649
response.setHeader('Content-Type', 'text/html');
16341650
response.setHeader('Content-Length', Buffer.byteLength(body));
@@ -1652,6 +1668,9 @@ added: v7.7.0
16521668
Returns an array containing the unique names of the current outgoing headers.
16531669
All header names are lowercase.
16541670

1671+
If `separateHeadersValues` was set to `true` when the server was created, an
1672+
header might be present in the list more than once.
1673+
16551674
```js
16561675
response.setHeader('Foo', 'bar');
16571676
response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']);
@@ -1679,6 +1698,10 @@ prototypically inherit from the JavaScript `Object`. This means that typical
16791698
`Object` methods such as `obj.toString()`, `obj.hasOwnProperty()`, and others
16801699
are not defined and _will not work_.
16811700

1701+
If `separateHeadersValues` was set to `true` when the server was created, then
1702+
the values are always a string if the header will be sent once or
1703+
an array of strings if the header will be sent multiple times.
1704+
16821705
```js
16831706
response.setHeader('Foo', 'bar');
16841707
response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']);
@@ -1788,6 +1811,10 @@ When headers have been set with [`response.setHeader()`][], they will be merged
17881811
with any headers passed to [`response.writeHead()`][], with the headers passed
17891812
to [`response.writeHead()`][] given precedence.
17901813

1814+
If `separateHeadersValues` was set to `true` when the request was created, then
1815+
using this method will always append a new value to the list of values for this
1816+
header.
1817+
17911818
```js
17921819
// Returns content-type = text/plain
17931820
const server = http.createServer((req, res) => {
@@ -2551,11 +2578,15 @@ added: v0.4.0
25512578
-->
25522579

25532580
* `name` {string} Name of header
2554-
* Returns {string | undefined}
2581+
* Returns {string | string\[] | undefined}
25552582

25562583
Gets the value of HTTP header with the given name. If such a name doesn't
25572584
exist in message, it will be `undefined`.
25582585

2586+
If `separateHeadersValues` was set to `true` when the request or the server
2587+
were created, then the return value is always a string if the header has been
2588+
set once or an array of strings if the header has been set multiple times.
2589+
25592590
### `outgoingMessage.getHeaderNames()`
25602591

25612592
<!-- YAML
@@ -2567,6 +2598,9 @@ added: v8.0.0
25672598
Returns an array of names of headers of the outgoing outgoingMessage. All
25682599
names are lowercase.
25692600

2601+
If `separateHeadersValues` was set to `true` when the request or the server
2602+
were created, an header might be present in the list more than once.
2603+
25702604
### `outgoingMessage.getHeaders()`
25712605

25722606
<!-- YAML
@@ -2594,6 +2628,10 @@ const headers = outgoingMessage.getHeaders();
25942628
// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] }
25952629
```
25962630

2631+
If `separateHeadersValues` was set to `true` when the request or the server
2632+
were created, then the values are always a string if the header will be sent
2633+
once or an array of strings if the header will be sent multiple times.
2634+
25972635
### `outgoingMessage.hasHeader(name)`
25982636

25992637
<!-- YAML
@@ -2659,6 +2697,10 @@ added: v0.4.0
26592697

26602698
Sets a single header value for the header object.
26612699

2700+
If `separateHeadersValues` was set to `true` when the request or the server
2701+
were created, then using this method will always append a new value to the list
2702+
of values for this header.
2703+
26622704
### `outgoingMessage.setTimeout(msesc[, callback])`
26632705

26642706
<!-- YAML
@@ -2865,6 +2907,14 @@ changes:
28652907
[`--max-http-header-size`][] for requests received by this server, i.e.
28662908
the maximum length of request headers in bytes.
28672909
**Default:** 16384 (16 KB).
2910+
* `separateHeadersValues` {boolean} If set to `true`, incoming requests and
2911+
server responses headers will never be squashed.
2912+
All incoming requests headers will always be returned as a string if the
2913+
header has been received once or as an array of strings if the header has
2914+
been received multiple times.
2915+
All server responses headers will be sent multiple times if the header
2916+
value was set to an array or set multiple times.
2917+
In both cases headers matching is case insensitive.
28682918

28692919
* `requestListener` {Function}
28702920

@@ -3072,6 +3122,14 @@ changes:
30723122
`hostname`. Valid values are `4` or `6`. When unspecified, both IP v4 and
30733123
v6 will be used.
30743124
* `headers` {Object} An object containing request headers.
3125+
* `separateHeadersValues` {boolean} If set to `true`, the request and the
3126+
response headers will never be squashed.
3127+
All request headers will be sent multiple times if the header
3128+
value was set to an array or set multiple times.
3129+
All response headers will always be returned as a string if the
3130+
header has been received once or as an array of strings if the header has
3131+
been received multiple times.
3132+
In both cases headers matching is case insensitive.
30753133
* `hints` {number} Optional [`dns.lookup()` hints][].
30763134
* `host` {string} A domain name or IP address of the server to issue the
30773135
request to. **Default:** `'localhost'`.

lib/_http_agent.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
const {
2525
ArrayPrototypeIncludes,
2626
ArrayPrototypeIndexOf,
27+
ArrayIsArray,
2728
ArrayPrototypePop,
2829
ArrayPrototypePush,
2930
ArrayPrototypeShift,
@@ -60,6 +61,7 @@ const {
6061
validateOneOf,
6162
validateString,
6263
} = require('internal/validators');
64+
const { kSeparateHeadersValues } = require('_http_incoming');
6365

6466
const kOnKeylog = Symbol('onkeylog');
6567
const kRequestOptions = Symbol('requestOptions');
@@ -344,7 +346,14 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {
344346

345347
function calculateServerName(options, req) {
346348
let servername = options.host;
347-
const hostHeader = req.getHeader('host');
349+
let hostHeader = req.getHeader('host');
350+
351+
// If the separateHeadersValues is active, the header might be specified
352+
// multiple times. In that case we choose the last value.
353+
if(req[kSeparateHeadersValues] && ArrayIsArray(hostHeader)) {
354+
hostHeader = hostHeader[hostHeader.length - 1];
355+
}
356+
348357
if (hostHeader) {
349358
validateString(hostHeader, 'options.headers.host');
350359

lib/_http_client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const assert = require('internal/assert');
4646
const { once } = require('internal/util');
4747
const {
4848
_checkIsHttpToken: checkIsHttpToken,
49+
kSeparateHeadersValues,
4950
freeParser,
5051
parsers,
5152
HTTPParser,
@@ -195,6 +196,7 @@ function ClientRequest(input, options, cb) {
195196
if (maxHeaderSize !== undefined)
196197
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
197198
this.maxHeaderSize = maxHeaderSize;
199+
this[kSeparateHeadersValues] = Boolean(options.separateHeadersValues);
198200

199201
const insecureHTTPParser = options.insecureHTTPParser;
200202
if (insecureHTTPParser !== undefined &&
@@ -726,6 +728,8 @@ function tickOnSocket(req, socket) {
726728
0);
727729
parser.socket = socket;
728730
parser.outgoing = req;
731+
parser[kSeparateHeadersValues] = req[kSeparateHeadersValues];
732+
729733
req.parser = parser;
730734

731735
socket.parser = parser;

lib/_http_common.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ const { getOptionValue } = require('internal/options');
3333
const insecureHTTPParser = getOptionValue('--insecure-http-parser');
3434

3535
const FreeList = require('internal/freelist');
36-
const incoming = require('_http_incoming');
3736
const {
37+
kSeparateHeadersValues,
3838
IncomingMessage,
3939
readStart,
4040
readStop
41-
} = incoming;
41+
} = require('_http_incoming');
4242

4343
let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
4444
debug = fn;
@@ -101,6 +101,7 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
101101
incoming.httpVersion = `${versionMajor}.${versionMinor}`;
102102
incoming.url = url;
103103
incoming.upgrade = upgrade;
104+
incoming[kSeparateHeadersValues] = parser[kSeparateHeadersValues];
104105

105106
if (socket) {
106107
debug('requestTimeout timer moved to req');
@@ -272,6 +273,7 @@ module.exports = {
272273
freeParser,
273274
methods,
274275
parsers,
276+
kSeparateHeadersValues,
275277
kIncomingMessage,
276278
kRequestTimeout,
277279
HTTPParser,

lib/_http_incoming.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
'use strict';
2323

2424
const {
25+
ArrayIsArray,
2526
ObjectDefineProperty,
2627
ObjectSetPrototypeOf,
2728
StringPrototypeCharCodeAt,
@@ -34,6 +35,7 @@ const { Readable, finished } = require('stream');
3435

3536
const kHeaders = Symbol('kHeaders');
3637
const kHeadersCount = Symbol('kHeadersCount');
38+
const kSeparateHeadersValues = Symbol('kSeparateHeadersValues');
3739
const kTrailers = Symbol('kTrailers');
3840
const kTrailersCount = Symbol('kTrailersCount');
3941

@@ -112,8 +114,14 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
112114
const src = this.rawHeaders;
113115
const dst = this[kHeaders];
114116

115-
for (let n = 0; n < this[kHeadersCount]; n += 2) {
116-
this._addHeaderLine(src[n + 0], src[n + 1], dst);
117+
if (this[kSeparateHeadersValues]) {
118+
for (let n = 0; n < this[kHeadersCount]; n += 2) {
119+
this._addHeaderLineSeparate(src[n + 0], src[n + 1], dst);
120+
}
121+
} else {
122+
for (let n = 0; n < this[kHeadersCount]; n += 2) {
123+
this._addHeaderLine(src[n + 0], src[n + 1], dst);
124+
}
117125
}
118126
}
119127
return this[kHeaders];
@@ -131,8 +139,14 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
131139
const src = this.rawTrailers;
132140
const dst = this[kTrailers];
133141

134-
for (let n = 0; n < this[kTrailersCount]; n += 2) {
135-
this._addHeaderLine(src[n + 0], src[n + 1], dst);
142+
if (this[kSeparateHeadersValues]) {
143+
for (let n = 0; n < this[kTrailersCount]; n += 2) {
144+
this._addHeaderLineSeparate(src[n + 0], src[n + 1], dst);
145+
}
146+
} else {
147+
for (let n = 0; n < this[kTrailersCount]; n += 2) {
148+
this._addHeaderLine(src[n + 0], src[n + 1], dst);
149+
}
136150
}
137151
}
138152
return this[kTrailers];
@@ -213,8 +227,14 @@ function _addHeaderLines(headers, n) {
213227
}
214228

215229
if (dest) {
216-
for (let i = 0; i < n; i += 2) {
217-
this._addHeaderLine(headers[i], headers[i + 1], dest);
230+
if (this[kSeparateHeadersValues]) {
231+
for (let i = 0; i < n; i += 2) {
232+
this._addHeaderLineSeparate(headers[i], headers[i + 1], dest);
233+
}
234+
} else {
235+
for (let i = 0; i < n; i += 2) {
236+
this._addHeaderLine(headers[i], headers[i + 1], dest);
237+
}
218238
}
219239
}
220240
}
@@ -361,6 +381,20 @@ function _addHeaderLine(field, value, dest) {
361381
}
362382
}
363383

384+
// Add the given (field, value) pair to the message, without joining values as
385+
// in _addHeaderLine. The headers values are always arrays.
386+
IncomingMessage.prototype._addHeaderLineSeparate = _addHeaderLineSeparate;
387+
function _addHeaderLineSeparate(field, value, dest) {
388+
field = StringPrototypeToLowerCase(field);
389+
390+
if(ArrayIsArray(dest[field])) {
391+
dest[field].push(value);
392+
} else if (dest[field] !== undefined) {
393+
dest[field] = [dest[field], value];
394+
} else {
395+
dest[field] = value;
396+
}
397+
}
364398

365399
// Call this instead of resume() if we want to just
366400
// dump all the data to /dev/null
@@ -385,6 +419,7 @@ function onError(self, error, cb) {
385419
}
386420

387421
module.exports = {
422+
kSeparateHeadersValues,
388423
IncomingMessage,
389424
readStart,
390425
readStop

0 commit comments

Comments
 (0)