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 <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent e0d2bc5 commit f0b2b26

File tree

4 files changed

+122
-103
lines changed

4 files changed

+122
-103
lines changed

benchmark/http/headers.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const http = require('http');
5+
6+
const bench = common.createBenchmark(main, {
7+
duplicates: [1, 100],
8+
n: [10, 1000],
9+
});
10+
11+
function main({ duplicates, n }) {
12+
const headers = {
13+
'Connection': 'keep-alive',
14+
'Transfer-Encoding': 'chunked',
15+
};
16+
17+
for (var i = 0; i < n / duplicates; i++) {
18+
headers[`foo${i}`] = [];
19+
for (var j = 0; j < duplicates; j++) {
20+
headers[`foo${i}`].push(`some header value ${i}`);
21+
}
22+
}
23+
24+
const server = 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

+79-99
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const { utcDate } = internalHttp;
5252

5353
const kIsCorked = Symbol('isCorked');
5454

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

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

166167
var headersMap = this[outHeadersKey];
167-
if (!headersMap) return {};
168-
169-
var headers = {};
170-
var keys = Object.keys(headersMap);
168+
const headers = {};
171169

172-
for (var i = 0, l = keys.length; i < l; i++) {
173-
var key = keys[i];
174-
headers[headersMap[key][0]] = headersMap[key][1];
170+
if (headersMap !== null) {
171+
const keys = Object.keys(headersMap);
172+
for (var i = 0, l = keys.length; i < l; i++) {
173+
const key = keys[i];
174+
headers[headersMap[key][0]] = headersMap[key][1];
175+
}
175176
}
176177
return headers;
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-
var state = {
289+
const state = {
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-
var field;
300299
var key;
301-
var value;
302-
var i;
303-
var j;
304300
if (headers === this[outHeadersKey]) {
305301
for (key in headers) {
306-
var entry = headers[key];
307-
field = entry[0];
308-
value = entry[1];
309-
310-
if (value instanceof Array) {
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+
const entry = headers[key];
303+
processHeader(this, state, entry[0], entry[1], false);
319304
}
320-
} else if (headers instanceof Array) {
321-
for (i = 0; i < headers.length; i++) {
322-
field = headers[i][0];
323-
value = headers[i][1];
324-
325-
if (value instanceof Array) {
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+
} else if (Array.isArray(headers)) {
306+
for (var i = 0; i < headers.length; i++) {
307+
const entry = headers[i];
308+
processHeader(this, state, entry[0], entry[1], true);
332309
}
333310
} else if (headers) {
334-
var keys = Object.keys(headers);
335-
for (i = 0; i < keys.length; i++) {
336-
field = keys[i];
337-
value = headers[field];
338-
339-
if (value instanceof Array) {
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 (key in headers) {
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-
var statusCode = 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
} else if (!state.connection) {
380-
var shouldSendKeepAlive = this.shouldKeepAlive &&
349+
const shouldSendKeepAlive = 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
} else if (!state.trailer &&
397366
!this._removedContLen &&
398367
typeof this._contentLength === 'number') {
399-
state.header += 'Content-Length: ' + this._contentLength + CRLF;
368+
header += 'Content-Length: ' + this._contentLength + CRLF;
400369
} else if (!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
throw new ERR_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-
function storeHeader(self, state, key, value, validate) {
428-
if (validate) {
429-
validateHeader(key, value);
396+
function processHeader(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 (var i = 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+
function storeHeader(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-
function validateHeader(name, value) {
464-
let err;
447+
function validateHeaderName(name) {
465448
if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) {
466-
err = new ERR_INVALID_HTTP_TOKEN('Header name', name);
467-
} else if (value === undefined) {
449+
const err = new ERR_INVALID_HTTP_TOKEN('Header name', name);
450+
Error.captureStackTrace(err, validateHeaderName);
451+
throw err;
452+
}
453+
}
454+
455+
function validateHeaderValue(name, value) {
456+
let err;
457+
if (value === undefined) {
468458
err = new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
469459
} else if (checkInvalidHeaderChar(value)) {
470460
debug('Header "%s" contains invalid characters', name);
471461
err = new ERR_INVALID_CHAR('header content', name);
472462
}
473463
if (err !== undefined) {
474-
Error.captureStackTrace(err, validateHeader);
464+
Error.captureStackTrace(err, validateHeaderValue);
475465
throw err;
476466
}
477467
}
@@ -480,25 +470,14 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
480470
if (this._header) {
481471
throw new ERR_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+
let headers = this[outHeadersKey];
477+
if (headers === null)
478+
this[outHeadersKey] = headers = Object.create(null);
487479

488-
const key = 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
throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
508487
}
509488

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

518497

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

524503

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

546-
return !!(this[outHeadersKey] && this[outHeadersKey][name.toLowerCase()]);
525+
return this[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
delete this[outHeadersKey][key];
578558
}
579559
};

0 commit comments

Comments
 (0)