Skip to content

Commit c00e4ad

Browse files
committed
http: optimize header storage and matching
This commit implements two optimizations when working with headers: * Avoid having to explicitly "render" headers and separately store the original casing for header names. * Match special header names using a single regular expression instead of testing one regular expression per header name. PR-URL: #10558 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Evan Lucas <[email protected]>
1 parent ec8910b commit c00e4ad

File tree

4 files changed

+139
-112
lines changed

4 files changed

+139
-112
lines changed

lib/_http_client.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,12 @@ function ClientRequest(options, cb) {
131131
self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n',
132132
options.headers);
133133
} else if (self.getHeader('expect')) {
134+
if (self._header) {
135+
throw new Error('Can\'t render headers after they are sent to the ' +
136+
'client');
137+
}
134138
self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n',
135-
self._renderHeaders());
139+
self._headers);
136140
}
137141

138142
this._ended = false;
@@ -224,8 +228,11 @@ ClientRequest.prototype._finish = function _finish() {
224228
};
225229

226230
ClientRequest.prototype._implicitHeader = function _implicitHeader() {
231+
if (this._header) {
232+
throw new Error('Can\'t render headers after they are sent to the client');
233+
}
227234
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
228-
this._renderHeaders());
235+
this._headers);
229236
};
230237

231238
ClientRequest.prototype.abort = function abort() {

lib/_http_common.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ const debug = require('util').debuglog('http');
1414
exports.debug = debug;
1515

1616
exports.CRLF = '\r\n';
17-
exports.chunkExpression = /chunk/i;
18-
exports.continueExpression = /100-continue/i;
17+
exports.chunkExpression = /(?:^|\W)chunked(?:$|\W)/i;
18+
exports.continueExpression = /(?:^|\W)100-continue(?:$|\W)/i;
1919
exports.methods = methods;
2020

2121
const kOnHeaders = HTTPParser.kOnHeaders | 0;

lib/_http_outgoing.js

+118-105
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,8 @@ const checkIsHttpToken = common._checkIsHttpToken;
1111
const checkInvalidHeaderChar = common._checkInvalidHeaderChar;
1212

1313
const CRLF = common.CRLF;
14-
const trfrEncChunkExpression = common.chunkExpression;
1514
const debug = common.debug;
1615

17-
const upgradeExpression = /^Upgrade$/i;
18-
const transferEncodingExpression = /^Transfer-Encoding$/i;
19-
const contentLengthExpression = /^Content-Length$/i;
20-
const dateExpression = /^Date$/i;
21-
const expectExpression = /^Expect$/i;
22-
const trailerExpression = /^Trailer$/i;
23-
const connectionExpression = /^Connection$/i;
24-
const connCloseExpression = /(^|\W)close(\W|$)/i;
25-
const connUpgradeExpression = /(^|\W)upgrade(\W|$)/i;
2616

2717
const automaticHeaders = {
2818
connection: true,
@@ -31,6 +21,10 @@ const automaticHeaders = {
3121
date: true
3222
};
3323

24+
var RE_FIELDS = new RegExp('^(?:Connection|Transfer-Encoding|Content-Length|' +
25+
'Date|Expect|Trailer|Upgrade)$', 'i');
26+
var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig;
27+
var RE_TE_CHUNKED = common.chunkExpression;
3428

3529
var dateCache;
3630
function utcDate() {
@@ -83,7 +77,6 @@ function OutgoingMessage() {
8377
this.connection = null;
8478
this._header = null;
8579
this._headers = null;
86-
this._headerNames = {};
8780

8881
this._onPendingData = null;
8982
}
@@ -198,57 +191,72 @@ function _storeHeader(firstLine, headers) {
198191
// firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
199192
// in the case of response it is: 'HTTP/1.1 200 OK\r\n'
200193
var state = {
201-
sentConnectionHeader: false,
202-
sentConnectionUpgrade: false,
203-
sentContentLengthHeader: false,
204-
sentTransferEncodingHeader: false,
205-
sentDateHeader: false,
206-
sentExpect: false,
207-
sentTrailer: false,
208-
sentUpgrade: false,
209-
messageHeader: firstLine
194+
connection: false,
195+
connUpgrade: false,
196+
contLen: false,
197+
te: false,
198+
date: false,
199+
expect: false,
200+
trailer: false,
201+
upgrade: false,
202+
header: firstLine
210203
};
211204

212-
var i;
213-
var j;
214205
var field;
206+
var key;
215207
var value;
216-
if (headers instanceof Array) {
217-
for (i = 0; i < headers.length; ++i) {
208+
var i;
209+
var j;
210+
if (headers === this._headers) {
211+
for (key in headers) {
212+
var entry = headers[key];
213+
field = entry[0];
214+
value = entry[1];
215+
216+
if (value instanceof Array) {
217+
for (j = 0; j < value.length; j++) {
218+
storeHeader(this, state, field, value[j], false);
219+
}
220+
} else {
221+
storeHeader(this, state, field, value, false);
222+
}
223+
}
224+
} else if (headers instanceof Array) {
225+
for (i = 0; i < headers.length; i++) {
218226
field = headers[i][0];
219227
value = headers[i][1];
220228

221229
if (value instanceof Array) {
222230
for (j = 0; j < value.length; j++) {
223-
storeHeader(this, state, field, value[j]);
231+
storeHeader(this, state, field, value[j], true);
224232
}
225233
} else {
226-
storeHeader(this, state, field, value);
234+
storeHeader(this, state, field, value, true);
227235
}
228236
}
229237
} else if (headers) {
230238
var keys = Object.keys(headers);
231-
for (i = 0; i < keys.length; ++i) {
239+
for (i = 0; i < keys.length; i++) {
232240
field = keys[i];
233241
value = headers[field];
234242

235243
if (value instanceof Array) {
236244
for (j = 0; j < value.length; j++) {
237-
storeHeader(this, state, field, value[j]);
245+
storeHeader(this, state, field, value[j], true);
238246
}
239247
} else {
240-
storeHeader(this, state, field, value);
248+
storeHeader(this, state, field, value, true);
241249
}
242250
}
243251
}
244252

245253
// Are we upgrading the connection?
246-
if (state.sentConnectionUpgrade && state.sentUpgrade)
254+
if (state.connUpgrade && state.upgrade)
247255
this.upgrading = true;
248256

249257
// Date header
250-
if (this.sendDate && !state.sentDateHeader) {
251-
state.messageHeader += 'Date: ' + utcDate() + CRLF;
258+
if (this.sendDate && !state.date) {
259+
state.header += 'Date: ' + utcDate() + CRLF;
252260
}
253261

254262
// Force the connection to close when the response is a 204 No Content or
@@ -274,33 +282,30 @@ function _storeHeader(firstLine, headers) {
274282
if (this._removedHeader.connection) {
275283
this._last = true;
276284
this.shouldKeepAlive = false;
277-
} else if (!state.sentConnectionHeader) {
285+
} else if (!state.connection) {
278286
var shouldSendKeepAlive = this.shouldKeepAlive &&
279-
(state.sentContentLengthHeader ||
280-
this.useChunkedEncodingByDefault ||
281-
this.agent);
287+
(state.contLen || this.useChunkedEncodingByDefault || this.agent);
282288
if (shouldSendKeepAlive) {
283-
state.messageHeader += 'Connection: keep-alive\r\n';
289+
state.header += 'Connection: keep-alive\r\n';
284290
} else {
285291
this._last = true;
286-
state.messageHeader += 'Connection: close\r\n';
292+
state.header += 'Connection: close\r\n';
287293
}
288294
}
289295

290-
if (!state.sentContentLengthHeader && !state.sentTransferEncodingHeader) {
296+
if (!state.contLen && !state.te) {
291297
if (!this._hasBody) {
292298
// Make sure we don't end the 0\r\n\r\n at the end of the message.
293299
this.chunkedEncoding = false;
294300
} else if (!this.useChunkedEncodingByDefault) {
295301
this._last = true;
296302
} else {
297-
if (!state.sentTrailer &&
298303
!this._removedHeader['content-length'] &&
304+
if (!state.trailer &&
299305
typeof this._contentLength === 'number') {
300-
state.messageHeader += 'Content-Length: ' + this._contentLength +
301-
'\r\n';
302306
} else if (!this._removedHeader['transfer-encoding']) {
303-
state.messageHeader += 'Transfer-Encoding: chunked\r\n';
307+
state.header += 'Content-Length: ' + this._contentLength + CRLF;
308+
state.header += 'Transfer-Encoding: chunked\r\n';
304309
this.chunkedEncoding = true;
305310
} else {
306311
// We should only be able to get here if both Content-Length and
@@ -311,70 +316,94 @@ function _storeHeader(firstLine, headers) {
311316
}
312317
}
313318

314-
this._header = state.messageHeader + CRLF;
319+
this._header = state.header + CRLF;
315320
this._headerSent = false;
316321

317322
// wait until the first body chunk, or close(), is sent to flush,
318323
// UNLESS we're sending Expect: 100-continue.
319-
if (state.sentExpect) this._send('');
324+
if (state.expect) this._send('');
320325
}
321326

322-
function storeHeader(self, state, field, value) {
323-
if (!checkIsHttpToken(field)) {
324-
throw new TypeError(
325-
'Header name must be a valid HTTP Token ["' + field + '"]');
326-
}
327-
if (checkInvalidHeaderChar(value)) {
328-
debug('Header "%s" contains invalid characters', field);
329-
throw new TypeError('The header content contains invalid characters');
330-
}
331-
state.messageHeader += field + ': ' + escapeHeaderValue(value) + CRLF;
332-
333-
if (connectionExpression.test(field)) {
334-
state.sentConnectionHeader = true;
335-
if (connCloseExpression.test(value)) {
336-
self._last = true;
337-
} else {
338-
self.shouldKeepAlive = true;
327+
function storeHeader(self, state, field, value, validate) {
328+
if (validate) {
329+
if (!checkIsHttpToken(field)) {
330+
throw new TypeError(
331+
'Header name must be a valid HTTP Token ["' + field + '"]');
332+
}
333+
if (value === undefined) {
334+
throw new Error('Header "%s" value must not be undefined', field);
335+
} else if (checkInvalidHeaderChar(value)) {
336+
debug('Header "%s" contains invalid characters', field);
337+
throw new TypeError('The header content contains invalid characters');
339338
}
340-
if (connUpgradeExpression.test(value))
341-
state.sentConnectionUpgrade = true;
342-
} else if (transferEncodingExpression.test(field)) {
343-
state.sentTransferEncodingHeader = true;
344-
if (trfrEncChunkExpression.test(value)) self.chunkedEncoding = true;
345-
346-
} else if (contentLengthExpression.test(field)) {
347-
state.sentContentLengthHeader = true;
348-
} else if (dateExpression.test(field)) {
349-
state.sentDateHeader = true;
350-
} else if (expectExpression.test(field)) {
351-
state.sentExpect = true;
352-
} else if (trailerExpression.test(field)) {
353-
state.sentTrailer = true;
354-
} else if (upgradeExpression.test(field)) {
355-
state.sentUpgrade = true;
356339
}
340+
state.header += field + ': ' + escapeHeaderValue(value) + CRLF;
341+
matchHeader(self, state, field, value);
357342
}
358343

344+
function matchConnValue(self, state, value) {
345+
var sawClose = false;
346+
var m = RE_CONN_VALUES.exec(value);
347+
while (m) {
348+
if (m[0].length === 5)
349+
sawClose = true;
350+
else
351+
state.connUpgrade = true;
352+
m = RE_CONN_VALUES.exec(value);
353+
}
354+
if (sawClose)
355+
self._last = true;
356+
else
357+
self.shouldKeepAlive = true;
358+
}
359359

360-
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
360+
function matchHeader(self, state, field, value) {
361+
var m = RE_FIELDS.exec(field);
362+
if (!m)
363+
return;
364+
var len = m[0].length;
365+
if (len === 10) {
366+
state.connection = true;
367+
matchConnValue(self, state, value);
368+
} else if (len === 17) {
369+
state.te = true;
370+
if (RE_TE_CHUNKED.test(value)) self.chunkedEncoding = true;
371+
} else if (len === 14) {
372+
state.contLen = true;
373+
} else if (len === 4) {
374+
state.date = true;
375+
} else if (len === 6) {
376+
state.expect = true;
377+
} else if (len === 7) {
378+
var ch = m[0].charCodeAt(0);
379+
if (ch === 85 || ch === 117)
380+
state.upgrade = true;
381+
else
382+
state.trailer = true;
383+
}
384+
}
385+
386+
function validateHeader(msg, name, value) {
361387
if (!checkIsHttpToken(name))
362388
throw new TypeError(
363389
'Header name must be a valid HTTP Token ["' + name + '"]');
364390
if (value === undefined)
365391
throw new Error('"value" required in setHeader("' + name + '", value)');
366-
if (this._header)
392+
if (msg._header)
367393
throw new Error('Can\'t set headers after they are sent.');
368394
if (checkInvalidHeaderChar(value)) {
369395
debug('Header "%s" contains invalid characters', name);
370396
throw new TypeError('The header content contains invalid characters');
371397
}
372-
if (this._headers === null)
398+
}
399+
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
400+
validateHeader(this, name, value);
401+
402+
if (!this._headers)
373403
this._headers = {};
374404

375-
var key = name.toLowerCase();
376-
this._headers[key] = value;
377-
this._headerNames[key] = name;
405+
const key = name.toLowerCase();
406+
this._headers[key] = [name, value];
378407

379408
if (automaticHeaders[key])
380409
this._removedHeader[key] = false;
@@ -388,7 +417,10 @@ OutgoingMessage.prototype.getHeader = function getHeader(name) {
388417

389418
if (!this._headers) return;
390419

391-
return this._headers[name.toLowerCase()];
420+
var entry = this._headers[name.toLowerCase()];
421+
if (!entry)
422+
return;
423+
return entry[1];
392424
};
393425

394426

@@ -410,30 +442,10 @@ OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
410442

411443
if (this._headers) {
412444
delete this._headers[key];
413-
delete this._headerNames[key];
414445
}
415446
};
416447

417448

418-
OutgoingMessage.prototype._renderHeaders = function _renderHeaders() {
419-
if (this._header) {
420-
throw new Error('Can\'t render headers after they are sent to the client');
421-
}
422-
423-
var headersMap = this._headers;
424-
if (!headersMap) return {};
425-
426-
var headers = {};
427-
var keys = Object.keys(headersMap);
428-
var headerNames = this._headerNames;
429-
430-
for (var i = 0, l = keys.length; i < l; i++) {
431-
var key = keys[i];
432-
headers[headerNames[key]] = headersMap[key];
433-
}
434-
return headers;
435-
};
436-
437449
OutgoingMessage.prototype._implicitHeader = function _implicitHeader() {
438450
throw new Error('_implicitHeader() method is not implemented');
439451
};
@@ -492,6 +504,7 @@ OutgoingMessage.prototype.write = function write(chunk, encoding, callback) {
492504
this.connection.cork();
493505
process.nextTick(connectionCorkNT, this.connection);
494506
}
507+
495508
this._send(len.toString(16), 'latin1', null);
496509
this._send(crlf_buf, null, null);
497510
this._send(chunk, encoding, null);

0 commit comments

Comments
 (0)