Skip to content

Commit 9d1b4b7

Browse files
ShogunPandatargos
authored andcommitted
http: add uniqueHeaders option to request and createServer
PR-URL: #41397 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent c41bf4d commit 9d1b4b7

File tree

6 files changed

+380
-13
lines changed

6 files changed

+380
-13
lines changed

doc/api/http.md

+67-5
Original file line numberDiff line numberDiff line change
@@ -2333,8 +2333,28 @@ header name:
23332333
`last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`,
23342334
`retry-after`, `server`, or `user-agent` are discarded.
23352335
* `set-cookie` is always an array. Duplicates are added to the array.
2336-
* For duplicate `cookie` headers, the values are joined together with '; '.
2337-
* For all other headers, the values are joined together with ', '.
2336+
* For duplicate `cookie` headers, the values are joined together with `; `.
2337+
* For all other headers, the values are joined together with `, `.
2338+
2339+
### `message.headersDistinct`
2340+
2341+
<!-- YAML
2342+
added: REPLACEME
2343+
-->
2344+
2345+
* {Object}
2346+
2347+
Similar to [`message.headers`][], but there is no join logic and the values are
2348+
always arrays of strings, even for headers received just once.
2349+
2350+
```js
2351+
// Prints something like:
2352+
//
2353+
// { 'user-agent': ['curl/7.22.0'],
2354+
// host: ['127.0.0.1:8000'],
2355+
// accept: ['*/*'] }
2356+
console.log(request.headersDistinct);
2357+
```
23382358

23392359
### `message.httpVersion`
23402360

@@ -2468,6 +2488,18 @@ added: v0.3.0
24682488

24692489
The request/response trailers object. Only populated at the `'end'` event.
24702490

2491+
### `message.trailersDistinct`
2492+
2493+
<!-- YAML
2494+
added: REPLACEME
2495+
-->
2496+
2497+
* {Object}
2498+
2499+
Similar to [`message.trailers`][], but there is no join logic and the values are
2500+
always arrays of strings, even for headers received just once.
2501+
Only populated at the `'end'` event.
2502+
24712503
### `message.url`
24722504

24732505
<!-- YAML
@@ -2565,7 +2597,7 @@ Adds HTTP trailers (headers but at the end of the message) to the message.
25652597
Trailers will **only** be emitted if the message is chunked encoded. If not,
25662598
the trailers will be silently discarded.
25672599

2568-
HTTP requires the `Trailer` header to be sent to emit trailers,
2600+
HTTP requires the `Trailer` header to be sent to emit trailers,
25692601
with a list of header field names in its value, e.g.
25702602

25712603
```js
@@ -2579,6 +2611,28 @@ message.end();
25792611
Attempting to set a header field name or value that contains invalid characters
25802612
will result in a `TypeError` being thrown.
25812613

2614+
### `outgoingMessage.appendHeader(name, value)`
2615+
2616+
<!-- YAML
2617+
added: REPLACEME
2618+
-->
2619+
2620+
* `name` {string} Header name
2621+
* `value` {string|string\[]} Header value
2622+
* Returns: {this}
2623+
2624+
Append a single header value for the header object.
2625+
2626+
If the value is an array, this is equivalent of calling this method multiple
2627+
times.
2628+
2629+
If there were no previous value for the header, this is equivalent of calling
2630+
[`outgoingMessage.setHeader(name, value)`][].
2631+
2632+
Depending of the value of `options.uniqueHeaders` when the client request or the
2633+
server were created, this will end up in the header being sent multiple times or
2634+
a single time with values joined using `; `.
2635+
25822636
### `outgoingMessage.connection`
25832637

25842638
<!-- YAML
@@ -2970,6 +3024,9 @@ changes:
29703024
* `keepAliveInitialDelay` {number} If set to a positive number, it sets the
29713025
initial delay before the first keepalive probe is sent on an idle socket.
29723026
**Default:** `0`.
3027+
* `uniqueHeaders` {Array} A list of response headers that should be sent only
3028+
once. If the header's value is an array, the items will be joined
3029+
using `; `.
29733030

29743031
* `requestListener` {Function}
29753032

@@ -3202,12 +3259,15 @@ changes:
32023259
* `protocol` {string} Protocol to use. **Default:** `'http:'`.
32033260
* `setHost` {boolean}: Specifies whether or not to automatically add the
32043261
`Host` header. Defaults to `true`.
3262+
* `signal` {AbortSignal}: An AbortSignal that may be used to abort an ongoing
3263+
request.
32053264
* `socketPath` {string} Unix domain socket. Cannot be used if one of `host`
32063265
or `port` is specified, as those specify a TCP Socket.
32073266
* `timeout` {number}: A number specifying the socket timeout in milliseconds.
32083267
This will set the timeout before the socket is connected.
3209-
* `signal` {AbortSignal}: An AbortSignal that may be used to abort an ongoing
3210-
request.
3268+
* `uniqueHeaders` {Array} A list of request headers that should be sent
3269+
only once. If the header's value is an array, the items will be joined
3270+
using `; `.
32113271
* `callback` {Function}
32123272
* Returns: {http.ClientRequest}
32133273

@@ -3513,11 +3573,13 @@ try {
35133573
[`http.request()`]: #httprequestoptions-callback
35143574
[`message.headers`]: #messageheaders
35153575
[`message.socket`]: #messagesocket
3576+
[`message.trailers`]: #messagetrailers
35163577
[`net.Server.close()`]: net.md#serverclosecallback
35173578
[`net.Server`]: net.md#class-netserver
35183579
[`net.Socket`]: net.md#class-netsocket
35193580
[`net.createConnection()`]: net.md#netcreateconnectionoptions-connectlistener
35203581
[`new URL()`]: url.md#new-urlinput-base
3582+
[`outgoingMessage.setHeader(name, value)`]: #outgoingmessagesetheadername-value
35213583
[`outgoingMessage.socket`]: #outgoingmessagesocket
35223584
[`removeHeader(name)`]: #requestremoveheadername
35233585
[`request.destroy()`]: #requestdestroyerror

lib/_http_client.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ const {
5555
isLenient,
5656
prepareError,
5757
} = require('_http_common');
58-
const { OutgoingMessage } = require('_http_outgoing');
58+
const {
59+
kUniqueHeaders,
60+
parseUniqueHeadersOption,
61+
OutgoingMessage
62+
} = require('_http_outgoing');
5963
const Agent = require('_http_agent');
6064
const { Buffer } = require('buffer');
6165
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
@@ -303,6 +307,8 @@ function ClientRequest(input, options, cb) {
303307
options.headers);
304308
}
305309

310+
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
311+
306312
let optsWithoutSignal = options;
307313
if (optsWithoutSignal.signal) {
308314
optsWithoutSignal = ObjectAssign({}, options);

lib/_http_incoming.js

+50
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ const {
3333
const { Readable, finished } = require('stream');
3434

3535
const kHeaders = Symbol('kHeaders');
36+
const kHeadersDistinct = Symbol('kHeadersDistinct');
3637
const kHeadersCount = Symbol('kHeadersCount');
3738
const kTrailers = Symbol('kTrailers');
39+
const kTrailersDistinct = Symbol('kTrailersDistinct');
3840
const kTrailersCount = Symbol('kTrailersCount');
3941

4042
function readStart(socket) {
@@ -123,6 +125,25 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
123125
}
124126
});
125127

128+
ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
129+
get: function() {
130+
if (!this[kHeadersDistinct]) {
131+
this[kHeadersDistinct] = {};
132+
133+
const src = this.rawHeaders;
134+
const dst = this[kHeadersDistinct];
135+
136+
for (let n = 0; n < this[kHeadersCount]; n += 2) {
137+
this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
138+
}
139+
}
140+
return this[kHeadersDistinct];
141+
},
142+
set: function(val) {
143+
this[kHeadersDistinct] = val;
144+
}
145+
});
146+
126147
ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
127148
get: function() {
128149
if (!this[kTrailers]) {
@@ -142,6 +163,25 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
142163
}
143164
});
144165

166+
ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
167+
get: function() {
168+
if (!this[kTrailersDistinct]) {
169+
this[kTrailersDistinct] = {};
170+
171+
const src = this.rawTrailers;
172+
const dst = this[kTrailersDistinct];
173+
174+
for (let n = 0; n < this[kTrailersCount]; n += 2) {
175+
this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
176+
}
177+
}
178+
return this[kTrailersDistinct];
179+
},
180+
set: function(val) {
181+
this[kTrailersDistinct] = val;
182+
}
183+
});
184+
145185
IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
146186
if (callback)
147187
this.on('timeout', callback);
@@ -358,6 +398,16 @@ function _addHeaderLine(field, value, dest) {
358398
}
359399
}
360400

401+
IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct;
402+
function _addHeaderLineDistinct(field, value, dest) {
403+
field = StringPrototypeToLowerCase(field);
404+
if (!dest[field]) {
405+
dest[field] = [value];
406+
} else {
407+
dest[field].push(value);
408+
}
409+
}
410+
361411

362412
// Call this instead of resume() if we want to just
363413
// dump all the data to /dev/null

lib/_http_outgoing.js

+76-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const {
3434
ObjectPrototypeHasOwnProperty,
3535
ObjectSetPrototypeOf,
3636
RegExpPrototypeTest,
37+
SafeSet,
3738
StringPrototypeToLowerCase,
3839
Symbol,
3940
} = primordials;
@@ -82,6 +83,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
8283
const HIGH_WATER_MARK = getDefaultHighWaterMark();
8384

8485
const kCorked = Symbol('corked');
86+
const kUniqueHeaders = Symbol('kUniqueHeaders');
8587

8688
const nop = () => {};
8789

@@ -502,7 +504,10 @@ function processHeader(self, state, key, value, validate) {
502504
if (validate)
503505
validateHeaderName(key);
504506
if (ArrayIsArray(value)) {
505-
if (value.length < 2 || !isCookieField(key)) {
507+
if (
508+
(value.length < 2 || !isCookieField(key)) &&
509+
(!self[kUniqueHeaders] || !self[kUniqueHeaders].has(StringPrototypeToLowerCase(key)))
510+
) {
506511
// Retain for(;;) loop for performance reasons
507512
// Refs: https://github.com/nodejs/node/pull/30958
508513
for (let i = 0; i < value.length; i++)
@@ -571,6 +576,20 @@ const validateHeaderValue = hideStackFrames((name, value) => {
571576
}
572577
});
573578

579+
function parseUniqueHeadersOption(headers) {
580+
if (!ArrayIsArray(headers)) {
581+
return null;
582+
}
583+
584+
const unique = new SafeSet();
585+
const l = headers.length;
586+
for (let i = 0; i < l; i++) {
587+
unique.add(StringPrototypeToLowerCase(headers[i]));
588+
}
589+
590+
return unique;
591+
}
592+
574593
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
575594
if (this._header) {
576595
throw new ERR_HTTP_HEADERS_SENT('set');
@@ -586,6 +605,36 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
586605
return this;
587606
};
588607

608+
OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
609+
if (this._header) {
610+
throw new ERR_HTTP_HEADERS_SENT('append');
611+
}
612+
validateHeaderName(name);
613+
validateHeaderValue(name, value);
614+
615+
const field = StringPrototypeToLowerCase(name);
616+
const headers = this[kOutHeaders];
617+
if (headers === null || !headers[field]) {
618+
return this.setHeader(name, value);
619+
}
620+
621+
// Prepare the field for appending, if required
622+
if (!ArrayIsArray(headers[field][1])) {
623+
headers[field][1] = [headers[field][1]];
624+
}
625+
626+
const existingValues = headers[field][1];
627+
if (ArrayIsArray(value)) {
628+
for (let i = 0, length = value.length; i < length; i++) {
629+
existingValues.push(value[i]);
630+
}
631+
} else {
632+
existingValues.push(value);
633+
}
634+
635+
return this;
636+
};
637+
589638

590639
OutgoingMessage.prototype.getHeader = function getHeader(name) {
591640
validateString(name, 'name');
@@ -797,7 +846,6 @@ function connectionCorkNT(conn) {
797846
conn.uncork();
798847
}
799848

800-
801849
OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
802850
this._trailer = '';
803851
const keys = ObjectKeys(headers);
@@ -817,11 +865,31 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
817865
if (typeof field !== 'string' || !field || !checkIsHttpToken(field)) {
818866
throw new ERR_INVALID_HTTP_TOKEN('Trailer name', field);
819867
}
820-
if (checkInvalidHeaderChar(value)) {
821-
debug('Trailer "%s" contains invalid characters', field);
822-
throw new ERR_INVALID_CHAR('trailer content', field);
868+
869+
// Check if the field must be sent several times
870+
const isArrayValue = ArrayIsArray(value);
871+
if (
872+
isArrayValue && value.length > 1 &&
873+
(!this[kUniqueHeaders] || !this[kUniqueHeaders].has(StringPrototypeToLowerCase(field)))
874+
) {
875+
for (let j = 0, l = value.length; j < l; j++) {
876+
if (checkInvalidHeaderChar(value[j])) {
877+
debug('Trailer "%s"[%d] contains invalid characters', field, j);
878+
throw new ERR_INVALID_CHAR('trailer content', field);
879+
}
880+
this._trailer += field + ': ' + value[j] + '\r\n';
881+
}
882+
} else {
883+
if (isArrayValue) {
884+
value = ArrayPrototypeJoin(value, '; ');
885+
}
886+
887+
if (checkInvalidHeaderChar(value)) {
888+
debug('Trailer "%s" contains invalid characters', field);
889+
throw new ERR_INVALID_CHAR('trailer content', field);
890+
}
891+
this._trailer += field + ': ' + value + '\r\n';
823892
}
824-
this._trailer += field + ': ' + value + '\r\n';
825893
}
826894
};
827895

@@ -997,6 +1065,8 @@ function(err, event) {
9971065
};
9981066

9991067
module.exports = {
1068+
kUniqueHeaders,
1069+
parseUniqueHeadersOption,
10001070
validateHeaderName,
10011071
validateHeaderValue,
10021072
OutgoingMessage

lib/_http_server.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ const {
4646
_checkInvalidHeaderChar: checkInvalidHeaderChar,
4747
prepareError,
4848
} = require('_http_common');
49-
const { OutgoingMessage } = require('_http_outgoing');
49+
const {
50+
kUniqueHeaders,
51+
parseUniqueHeadersOption,
52+
OutgoingMessage
53+
} = require('_http_outgoing');
5054
const {
5155
kOutHeaders,
5256
kNeedDrain,
@@ -404,6 +408,7 @@ function Server(options, requestListener) {
404408
this.maxRequestsPerSocket = 0;
405409
this.headersTimeout = 60 * 1000; // 60 seconds
406410
this.requestTimeout = 0;
411+
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
407412
}
408413
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
409414
ObjectSetPrototypeOf(Server, net.Server);
@@ -886,6 +891,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
886891
socket, state);
887892

888893
res.shouldKeepAlive = keepAlive;
894+
res[kUniqueHeaders] = server[kUniqueHeaders];
889895
DTRACE_HTTP_SERVER_REQUEST(req, socket);
890896

891897
if (onRequestStartChannel.hasSubscribers) {

0 commit comments

Comments
 (0)