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‎

Lines changed: 67 additions & 5 deletions
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‎

Lines changed: 7 additions & 1 deletion
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
constAgent=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
letoptsWithoutSignal=options;
307313
if(optsWithoutSignal.signal){
308314
optsWithoutSignal=ObjectAssign({},options);

‎lib/_http_incoming.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ const{
3333
const{ Readable, finished }=require('stream');
3434

3535
constkHeaders=Symbol('kHeaders');
36+
constkHeadersDistinct=Symbol('kHeadersDistinct');
3637
constkHeadersCount=Symbol('kHeadersCount');
3738
constkTrailers=Symbol('kTrailers');
39+
constkTrailersDistinct=Symbol('kTrailersDistinct');
3840
constkTrailersCount=Symbol('kTrailersCount');
3941

4042
functionreadStart(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+
constsrc=this.rawHeaders;
134+
constdst=this[kHeadersDistinct];
135+
136+
for(letn=0;n<this[kHeadersCount];n+=2){
137+
this._addHeaderLineDistinct(src[n+0],src[n+1],dst);
138+
}
139+
}
140+
returnthis[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+
constsrc=this.rawTrailers;
172+
constdst=this[kTrailersDistinct];
173+
174+
for(letn=0;n<this[kTrailersCount];n+=2){
175+
this._addHeaderLineDistinct(src[n+0],src[n+1],dst);
176+
}
177+
}
178+
returnthis[kTrailersDistinct];
179+
},
180+
set: function(val){
181+
this[kTrailersDistinct]=val;
182+
}
183+
});
184+
145185
IncomingMessage.prototype.setTimeout=functionsetTimeout(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‎

Lines changed: 76 additions & 6 deletions
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
constHIGH_WATER_MARK=getDefaultHighWaterMark();
8384

8485
constkCorked=Symbol('corked');
86+
constkUniqueHeaders=Symbol('kUniqueHeaders');
8587

8688
constnop=()=>{};
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(leti=0;i<value.length;i++)
@@ -571,6 +576,20 @@ const validateHeaderValue = hideStackFrames((name, value) =>{
571576
}
572577
});
573578

579+
functionparseUniqueHeadersOption(headers){
580+
if(!ArrayIsArray(headers)){
581+
returnnull;
582+
}
583+
584+
constunique=newSafeSet();
585+
constl=headers.length;
586+
for(leti=0;i<l;i++){
587+
unique.add(StringPrototypeToLowerCase(headers[i]));
588+
}
589+
590+
returnunique;
591+
}
592+
574593
OutgoingMessage.prototype.setHeader=functionsetHeader(name,value){
575594
if(this._header){
576595
thrownewERR_HTTP_HEADERS_SENT('set');
@@ -586,6 +605,36 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value){
586605
returnthis;
587606
};
588607

608+
OutgoingMessage.prototype.appendHeader=functionappendHeader(name,value){
609+
if(this._header){
610+
thrownewERR_HTTP_HEADERS_SENT('append');
611+
}
612+
validateHeaderName(name);
613+
validateHeaderValue(name,value);
614+
615+
constfield=StringPrototypeToLowerCase(name);
616+
constheaders=this[kOutHeaders];
617+
if(headers===null||!headers[field]){
618+
returnthis.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+
constexistingValues=headers[field][1];
627+
if(ArrayIsArray(value)){
628+
for(leti=0,length=value.length;i<length;i++){
629+
existingValues.push(value[i]);
630+
}
631+
}else{
632+
existingValues.push(value);
633+
}
634+
635+
returnthis;
636+
};
637+
589638

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

800-
801849
OutgoingMessage.prototype.addTrailers=functionaddTrailers(headers){
802850
this._trailer='';
803851
constkeys=ObjectKeys(headers);
@@ -817,11 +865,31 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers){
817865
if(typeoffield!=='string'||!field||!checkIsHttpToken(field)){
818866
thrownewERR_INVALID_HTTP_TOKEN('Trailer name',field);
819867
}
820-
if(checkInvalidHeaderChar(value)){
821-
debug('Trailer "%s" contains invalid characters',field);
822-
thrownewERR_INVALID_CHAR('trailer content',field);
868+
869+
// Check if the field must be sent several times
870+
constisArrayValue=ArrayIsArray(value);
871+
if(
872+
isArrayValue&&value.length>1&&
873+
(!this[kUniqueHeaders]||!this[kUniqueHeaders].has(StringPrototypeToLowerCase(field)))
874+
){
875+
for(letj=0,l=value.length;j<l;j++){
876+
if(checkInvalidHeaderChar(value[j])){
877+
debug('Trailer "%s"[%d] contains invalid characters',field,j);
878+
thrownewERR_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+
thrownewERR_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‎

Lines changed: 7 additions & 1 deletion
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)