Skip to content

Commit f0b2b26

Browse files
apapirovskiMylesBorins
authored andcommitted
http: refactor outgoing headers processing
Use a shared function, for..in instead of Object.keys, do less work in `setHeader` and instead defer some of it until later, and other minor changes to improve clarity, as well as a slight boost in performance. PR-URL: #20250 Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e0d2bc5 commit f0b2b26

4 files changed

Lines changed: 122 additions & 103 deletions

File tree

‎benchmark/http/headers.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
constcommon=require('../common.js');
4+
consthttp=require('http');
5+
6+
constbench=common.createBenchmark(main,{
7+
duplicates: [1,100],
8+
n: [10,1000],
9+
});
10+
11+
functionmain({ duplicates, n }){
12+
constheaders={
13+
'Connection': 'keep-alive',
14+
'Transfer-Encoding': 'chunked',
15+
};
16+
17+
for(vari=0;i<n/duplicates;i++){
18+
headers[`foo${i}`]=[];
19+
for(varj=0;j<duplicates;j++){
20+
headers[`foo${i}`].push(`some header value ${i}`);
21+
}
22+
}
23+
24+
constserver=http.createServer(function(req,res){
25+
res.writeHead(200,headers);
26+
res.end();
27+
});
28+
server.listen(common.PORT,function(){
29+
bench.http({
30+
path: '/',
31+
connections: 10
32+
},function(){
33+
server.close();
34+
});
35+
});
36+
}

‎lib/_http_outgoing.js‎

Lines changed: 79 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const { utcDate } = internalHttp;
5252

5353
constkIsCorked=Symbol('isCorked');
5454

55+
consthasOwnProperty=Function.call.bind(Object.prototype.hasOwnProperty);
56+
5557
varRE_CONN_CLOSE=/(?:^|\W)close(?:$|\W)/i;
5658
varRE_TE_CHUNKED=common.chunkExpression;
5759

@@ -116,7 +118,7 @@ Object.defineProperty(OutgoingMessage.prototype, '_headers', {
116118
if(val==null){
117119
this[outHeadersKey]=null;
118120
}elseif(typeofval==='object'){
119-
constheaders=this[outHeadersKey]={};
121+
constheaders=this[outHeadersKey]=Object.create(null);
120122
constkeys=Object.keys(val);
121123
for(vari=0;i<keys.length;++i){
122124
constname=keys[i];
@@ -129,7 +131,7 @@ Object.defineProperty(OutgoingMessage.prototype, '_headers', {
129131
Object.defineProperty(OutgoingMessage.prototype,'_headerNames',{
130132
get: function(){
131133
constheaders=this[outHeadersKey];
132-
if(headers){
134+
if(headers!==null){
133135
constout=Object.create(null);
134136
constkeys=Object.keys(headers);
135137
for(vari=0;i<keys.length;++i){
@@ -138,9 +140,8 @@ Object.defineProperty(OutgoingMessage.prototype, '_headerNames', {
138140
out[key]=val;
139141
}
140142
returnout;
141-
}else{
142-
returnheaders;
143143
}
144+
returnnull;
144145
},
145146
set: function(val){
146147
if(typeofval==='object'&&val!==null){
@@ -164,14 +165,14 @@ OutgoingMessage.prototype._renderHeaders = function _renderHeaders() {
164165
}
165166

166167
varheadersMap=this[outHeadersKey];
167-
if(!headersMap)return{};
168-
169-
varheaders={};
170-
varkeys=Object.keys(headersMap);
168+
constheaders={};
171169

172-
for(vari=0,l=keys.length;i<l;i++){
173-
varkey=keys[i];
174-
headers[headersMap[key][0]]=headersMap[key][1];
170+
if(headersMap!==null){
171+
constkeys=Object.keys(headersMap);
172+
for(vari=0,l=keys.length;i<l;i++){
173+
constkey=keys[i];
174+
headers[headersMap[key][0]]=headersMap[key][1];
175+
}
175176
}
176177
returnheaders;
177178
};
@@ -285,72 +286,40 @@ OutgoingMessage.prototype._storeHeader = _storeHeader;
285286
function_storeHeader(firstLine,headers){
286287
// firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
287288
// in the case of response it is: 'HTTP/1.1 200 OK\r\n'
288-
varstate={
289+
conststate={
289290
connection: false,
290291
contLen: false,
291292
te: false,
292293
date: false,
293294
expect: false,
294295
trailer: false,
295-
upgrade: false,
296296
header: firstLine
297297
};
298298

299-
varfield;
300299
varkey;
301-
varvalue;
302-
vari;
303-
varj;
304300
if(headers===this[outHeadersKey]){
305301
for(keyinheaders){
306-
varentry=headers[key];
307-
field=entry[0];
308-
value=entry[1];
309-
310-
if(valueinstanceofArray){
311-
if(value.length<2||!isCookieField(field)){
312-
for(j=0;j<value.length;j++)
313-
storeHeader(this,state,field,value[j],false);
314-
continue;
315-
}
316-
value=value.join('; ');
317-
}
318-
storeHeader(this,state,field,value,false);
302+
constentry=headers[key];
303+
processHeader(this,state,entry[0],entry[1],false);
319304
}
320-
}elseif(headersinstanceofArray){
321-
for(i=0;i<headers.length;i++){
322-
field=headers[i][0];
323-
value=headers[i][1];
324-
325-
if(valueinstanceofArray){
326-
for(j=0;j<value.length;j++){
327-
storeHeader(this,state,field,value[j],true);
328-
}
329-
}else{
330-
storeHeader(this,state,field,value,true);
331-
}
305+
}elseif(Array.isArray(headers)){
306+
for(vari=0;i<headers.length;i++){
307+
constentry=headers[i];
308+
processHeader(this,state,entry[0],entry[1],true);
332309
}
333310
}elseif(headers){
334-
varkeys=Object.keys(headers);
335-
for(i=0;i<keys.length;i++){
336-
field=keys[i];
337-
value=headers[field];
338-
339-
if(valueinstanceofArray){
340-
if(value.length<2||!isCookieField(field)){
341-
for(j=0;j<value.length;j++)
342-
storeHeader(this,state,field,value[j],true);
343-
continue;
344-
}
345-
value=value.join('; ');
311+
for(keyinheaders){
312+
if(hasOwnProperty(headers,key)){
313+
processHeader(this,state,key,headers[key],true);
346314
}
347-
storeHeader(this,state,field,value,true);
348315
}
349316
}
350317

318+
let{ header }=state;
319+
351320
// Date header
352321
if(this.sendDate&&!state.date){
353-
state.header+='Date: '+utcDate()+CRLF;
322+
header+='Date: '+utcDate()+CRLF;
354323
}
355324

356325
// Force the connection to close when the response is a 204 No Content or
@@ -364,9 +333,9 @@ function _storeHeader(firstLine, headers) {
364333
// It was pointed out that this might confuse reverse proxies to the point
365334
// of creating security liabilities, so suppress the zero chunk and force
366335
// the connection to close.
367-
varstatusCode=this.statusCode;
368-
if((statusCode===204||statusCode===304)&&this.chunkedEncoding){
369-
debug(statusCode+' response should not use chunked encoding,'+
336+
if(this.chunkedEncoding&&(this.statusCode===204||
337+
this.statusCode===304)){
338+
debug(this.statusCode+' response should not use chunked encoding,'+
370339
' closing connection.');
371340
this.chunkedEncoding=false;
372341
this.shouldKeepAlive=false;
@@ -377,13 +346,13 @@ function _storeHeader(firstLine, headers) {
377346
this._last=true;
378347
this.shouldKeepAlive=false;
379348
}elseif(!state.connection){
380-
varshouldSendKeepAlive=this.shouldKeepAlive&&
349+
constshouldSendKeepAlive=this.shouldKeepAlive&&
381350
(state.contLen||this.useChunkedEncodingByDefault||this.agent);
382351
if(shouldSendKeepAlive){
383-
state.header+='Connection: keep-alive\r\n';
352+
header+='Connection: keep-alive\r\n';
384353
}else{
385354
this._last=true;
386-
state.header+='Connection: close\r\n';
355+
header+='Connection: close\r\n';
387356
}
388357
}
389358

@@ -396,9 +365,9 @@ function _storeHeader(firstLine, headers) {
396365
}elseif(!state.trailer&&
397366
!this._removedContLen&&
398367
typeofthis._contentLength==='number'){
399-
state.header+='Content-Length: '+this._contentLength+CRLF;
368+
header+='Content-Length: '+this._contentLength+CRLF;
400369
}elseif(!this._removedTE){
401-
state.header+='Transfer-Encoding: chunked\r\n';
370+
header+='Transfer-Encoding: chunked\r\n';
402371
this.chunkedEncoding=true;
403372
}else{
404373
// We should only be able to get here if both Content-Length and
@@ -416,18 +385,31 @@ function _storeHeader(firstLine, headers) {
416385
thrownewERR_HTTP_TRAILER_INVALID();
417386
}
418387

419-
this._header=state.header+CRLF;
388+
this._header=header+CRLF;
420389
this._headerSent=false;
421390

422391
// wait until the first body chunk, or close(), is sent to flush,
423392
// UNLESS we're sending Expect: 100-continue.
424393
if(state.expect)this._send('');
425394
}
426395

427-
functionstoreHeader(self,state,key,value,validate){
428-
if(validate){
429-
validateHeader(key,value);
396+
functionprocessHeader(self,state,key,value,validate){
397+
if(validate)
398+
validateHeaderName(key);
399+
if(Array.isArray(value)){
400+
if(value.length<2||!isCookieField(key)){
401+
for(vari=0;i<value.length;i++)
402+
storeHeader(self,state,key,value[i],validate);
403+
return;
404+
}
405+
value=value.join('; ');
430406
}
407+
storeHeader(self,state,key,value,validate);
408+
}
409+
410+
functionstoreHeader(self,state,key,value,validate){
411+
if(validate)
412+
validateHeaderValue(key,value);
431413
state.header+=key+': '+escapeHeaderValue(value)+CRLF;
432414
matchHeader(self,state,key,value);
433415
}
@@ -439,39 +421,47 @@ function matchHeader(self, state, field, value) {
439421
switch(field){
440422
case'connection':
441423
state.connection=true;
424+
self._removedConnection=false;
442425
if(RE_CONN_CLOSE.test(value))
443426
self._last=true;
444427
else
445428
self.shouldKeepAlive=true;
446429
break;
447430
case'transfer-encoding':
448431
state.te=true;
432+
self._removedTE=false;
449433
if(RE_TE_CHUNKED.test(value))self.chunkedEncoding=true;
450434
break;
451435
case'content-length':
452436
state.contLen=true;
437+
self._removedContLen=false;
453438
break;
454439
case'date':
455440
case'expect':
456441
case'trailer':
457-
case'upgrade':
458442
state[field]=true;
459443
break;
460444
}
461445
}
462446

463-
functionvalidateHeader(name,value){
464-
leterr;
447+
functionvalidateHeaderName(name){
465448
if(typeofname!=='string'||!name||!checkIsHttpToken(name)){
466-
err=newERR_INVALID_HTTP_TOKEN('Header name',name);
467-
}elseif(value===undefined){
449+
consterr=newERR_INVALID_HTTP_TOKEN('Header name',name);
450+
Error.captureStackTrace(err,validateHeaderName);
451+
throwerr;
452+
}
453+
}
454+
455+
functionvalidateHeaderValue(name,value){
456+
leterr;
457+
if(value===undefined){
468458
err=newERR_HTTP_INVALID_HEADER_VALUE(value,name);
469459
}elseif(checkInvalidHeaderChar(value)){
470460
debug('Header "%s" contains invalid characters',name);
471461
err=newERR_INVALID_CHAR('header content',name);
472462
}
473463
if(err!==undefined){
474-
Error.captureStackTrace(err,validateHeader);
464+
Error.captureStackTrace(err,validateHeaderValue);
475465
throwerr;
476466
}
477467
}
@@ -480,25 +470,14 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
480470
if(this._header){
481471
thrownewERR_HTTP_HEADERS_SENT('set');
482472
}
483-
validateHeader(name,value);
473+
validateHeaderName(name);
474+
validateHeaderValue(name,value);
484475

485-
if(!this[outHeadersKey])
486-
this[outHeadersKey]={};
476+
letheaders=this[outHeadersKey];
477+
if(headers===null)
478+
this[outHeadersKey]=headers=Object.create(null);
487479

488-
constkey=name.toLowerCase();
489-
this[outHeadersKey][key]=[name,value];
490-
491-
switch(key){
492-
case'connection':
493-
this._removedConnection=false;
494-
break;
495-
case'content-length':
496-
this._removedContLen=false;
497-
break;
498-
case'transfer-encoding':
499-
this._removedTE=false;
500-
break;
501-
}
480+
headers[name.toLowerCase()]=[name,value];
502481
};
503482

504483

@@ -507,18 +486,18 @@ OutgoingMessage.prototype.getHeader = function getHeader(name) {
507486
thrownewERR_INVALID_ARG_TYPE('name','string',name);
508487
}
509488

510-
if(!this[outHeadersKey])return;
511-
512-
varentry=this[outHeadersKey][name.toLowerCase()];
513-
if(!entry)
489+
constheaders=this[outHeadersKey];
490+
if(headers===null)
514491
return;
515-
returnentry[1];
492+
493+
constentry=headers[name.toLowerCase()];
494+
returnentry&&entry[1];
516495
};
517496

518497

519498
// Returns an array of the names of the current outgoing headers.
520499
OutgoingMessage.prototype.getHeaderNames=functiongetHeaderNames(){
521-
return(this[outHeadersKey] ? Object.keys(this[outHeadersKey]) : []);
500+
returnthis[outHeadersKey]!==null? Object.keys(this[outHeadersKey]) : [];
522501
};
523502

524503

@@ -543,7 +522,8 @@ OutgoingMessage.prototype.hasHeader = function hasHeader(name) {
543522
thrownewERR_INVALID_ARG_TYPE('name','string',name);
544523
}
545524

546-
return!!(this[outHeadersKey]&&this[outHeadersKey][name.toLowerCase()]);
525+
returnthis[outHeadersKey]!==null&&
526+
!!this[outHeadersKey][name.toLowerCase()];
547527
};
548528

549529

@@ -573,7 +553,7 @@ OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
573553
break;
574554
}
575555

576-
if(this[outHeadersKey]){
556+
if(this[outHeadersKey]!==null){
577557
deletethis[outHeadersKey][key];
578558
}
579559
};

0 commit comments

Comments
 (0)