Skip to content

Commit 9539cfa

Browse files
ShogunPandabengl
authored andcommitted
http: add uniqueHeaders option to request and createServer
PR-URL: #41397 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent b772c13 commit 9539cfa

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
@@ -2364,8 +2364,28 @@ header name:
23642364
`last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`,
23652365
`retry-after`, `server`, or `user-agent` are discarded.
23662366
* `set-cookie` is always an array. Duplicates are added to the array.
2367-
* For duplicate `cookie` headers, the values are joined together with '; '.
2368-
* For all other headers, the values are joined together with ', '.
2367+
* For duplicate `cookie` headers, the values are joined together with `; `.
2368+
* For all other headers, the values are joined together with `, `.
2369+
2370+
### `message.headersDistinct`
2371+
2372+
<!-- YAML
2373+
added: REPLACEME
2374+
-->
2375+
2376+
* {Object}
2377+
2378+
Similar to [`message.headers`][], but there is no join logic and the values are
2379+
always arrays of strings, even for headers received just once.
2380+
2381+
```js
2382+
// Prints something like:
2383+
//
2384+
// { 'user-agent': ['curl/7.22.0'],
2385+
// host: ['127.0.0.1:8000'],
2386+
// accept: ['*/*'] }
2387+
console.log(request.headersDistinct);
2388+
```
23692389

23702390
### `message.httpVersion`
23712391

@@ -2499,6 +2519,18 @@ added: v0.3.0
24992519

25002520
The request/response trailers object. Only populated at the `'end'` event.
25012521

2522+
### `message.trailersDistinct`
2523+
2524+
<!-- YAML
2525+
added: REPLACEME
2526+
-->
2527+
2528+
* {Object}
2529+
2530+
Similar to [`message.trailers`][], but there is no join logic and the values are
2531+
always arrays of strings, even for headers received just once.
2532+
Only populated at the `'end'` event.
2533+
25022534
### `message.url`
25032535

25042536
<!-- YAML
@@ -2596,7 +2628,7 @@ Adds HTTP trailers (headers but at the end of the message) to the message.
25962628
Trailers will **only** be emitted if the message is chunked encoded. If not,
25972629
the trailers will be silently discarded.
25982630

2599-
HTTP requires the `Trailer` header to be sent to emit trailers,
2631+
HTTP requires the `Trailer` header to be sent to emit trailers,
26002632
with a list of header field names in its value, e.g.
26012633

26022634
```js
@@ -2610,6 +2642,28 @@ message.end();
26102642
Attempting to set a header field name or value that contains invalid characters
26112643
will result in a `TypeError` being thrown.
26122644

2645+
### `outgoingMessage.appendHeader(name, value)`
2646+
2647+
<!-- YAML
2648+
added: REPLACEME
2649+
-->
2650+
2651+
* `name` {string} Header name
2652+
* `value` {string|string\[]} Header value
2653+
* Returns: {this}
2654+
2655+
Append a single header value for the header object.
2656+
2657+
If the value is an array, this is equivalent of calling this method multiple
2658+
times.
2659+
2660+
If there were no previous value for the header, this is equivalent of calling
2661+
[`outgoingMessage.setHeader(name, value)`][].
2662+
2663+
Depending of the value of `options.uniqueHeaders` when the client request or the
2664+
server were created, this will end up in the header being sent multiple times or
2665+
a single time with values joined using `; `.
2666+
26132667
### `outgoingMessage.connection`
26142668

26152669
<!-- YAML
@@ -3026,6 +3080,9 @@ changes:
30263080
* `keepAliveInitialDelay` {number} If set to a positive number, it sets the
30273081
initial delay before the first keepalive probe is sent on an idle socket.
30283082
**Default:** `0`.
3083+
* `uniqueHeaders` {Array} A list of response headers that should be sent only
3084+
once. If the header's value is an array, the items will be joined
3085+
using `; `.
30293086

30303087
* `requestListener` {Function}
30313088

@@ -3260,12 +3317,15 @@ changes:
32603317
* `protocol` {string} Protocol to use. **Default:** `'http:'`.
32613318
* `setHost` {boolean}: Specifies whether or not to automatically add the
32623319
`Host` header. Defaults to `true`.
3320+
* `signal` {AbortSignal}: An AbortSignal that may be used to abort an ongoing
3321+
request.
32633322
* `socketPath` {string} Unix domain socket. Cannot be used if one of `host`
32643323
or `port` is specified, as those specify a TCP Socket.
32653324
* `timeout` {number}: A number specifying the socket timeout in milliseconds.
32663325
This will set the timeout before the socket is connected.
3267-
* `signal` {AbortSignal}: An AbortSignal that may be used to abort an ongoing
3268-
request.
3326+
* `uniqueHeaders` {Array} A list of request headers that should be sent
3327+
only once. If the header's value is an array, the items will be joined
3328+
using `; `.
32693329
* `callback` {Function}
32703330
* Returns: {http.ClientRequest}
32713331

@@ -3571,11 +3631,13 @@ try {
35713631
[`http.request()`]: #httprequestoptions-callback
35723632
[`message.headers`]: #messageheaders
35733633
[`message.socket`]: #messagesocket
3634+
[`message.trailers`]: #messagetrailers
35743635
[`net.Server.close()`]: net.md#serverclosecallback
35753636
[`net.Server`]: net.md#class-netserver
35763637
[`net.Socket`]: net.md#class-netsocket
35773638
[`net.createConnection()`]: net.md#netcreateconnectionoptions-connectlistener
35783639
[`new URL()`]: url.md#new-urlinput-base
3640+
[`outgoingMessage.setHeader(name, value)`]: #outgoingmessagesetheadername-value
35793641
[`outgoingMessage.socket`]: #outgoingmessagesocket
35803642
[`removeHeader(name)`]: #requestremoveheadername
35813643
[`request.destroy()`]: #requestdestroyerror

lib/_http_client.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ const {
5252
isLenient,
5353
prepareError,
5454
} = require('_http_common');
55-
const { OutgoingMessage } = require('_http_outgoing');
55+
const {
56+
kUniqueHeaders,
57+
parseUniqueHeadersOption,
58+
OutgoingMessage
59+
} = require('_http_outgoing');
5660
const Agent = require('_http_agent');
5761
const { Buffer } = require('buffer');
5862
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
@@ -300,6 +304,8 @@ function ClientRequest(input, options, cb) {
300304
options.headers);
301305
}
302306

307+
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
308+
303309
let optsWithoutSignal = options;
304310
if (optsWithoutSignal.signal) {
305311
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);
@@ -361,6 +401,16 @@ function _addHeaderLine(field, value, dest) {
361401
}
362402
}
363403

404+
IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct;
405+
function _addHeaderLineDistinct(field, value, dest) {
406+
field = StringPrototypeToLowerCase(field);
407+
if (!dest[field]) {
408+
dest[field] = [value];
409+
} else {
410+
dest[field].push(value);
411+
}
412+
}
413+
364414

365415
// Call this instead of resume() if we want to just
366416
// 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
@@ -47,7 +47,11 @@ const {
4747
prepareError,
4848
} = require('_http_common');
4949
const { ConnectionsList } = internalBinding('http_parser');
50-
const { OutgoingMessage } = require('_http_outgoing');
50+
const {
51+
kUniqueHeaders,
52+
parseUniqueHeadersOption,
53+
OutgoingMessage
54+
} = require('_http_outgoing');
5155
const {
5256
kOutHeaders,
5357
kNeedDrain,
@@ -450,6 +454,7 @@ function Server(options, requestListener) {
450454
this.maxHeadersCount = null;
451455
this.maxRequestsPerSocket = 0;
452456
setupConnectionsTracking(this);
457+
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
453458
}
454459
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
455460
ObjectSetPrototypeOf(Server, net.Server);
@@ -916,6 +921,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
916921
socket, state);
917922

918923
res.shouldKeepAlive = keepAlive;
924+
res[kUniqueHeaders] = server[kUniqueHeaders];
919925
DTRACE_HTTP_SERVER_REQUEST(req, socket);
920926

921927
if (onRequestStartChannel.hasSubscribers) {

0 commit comments

Comments
 (0)