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 <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
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‎

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

Lines changed: 7 additions & 1 deletion
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
constAgent=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
letoptsWithoutSignal=options;
304310
if(optsWithoutSignal.signal){
305311
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);
@@ -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‎

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
@@ -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)