Skip to content

Commit fbe7035

Browse files
committed
http: cut per-request work on the server hot path
Avoid building req.headers for Host, Expect, and body-header checks by scanning rawHeaders without allocating lowercased names. Cache status lines, the Date header, and Keep-Alive header pairs. Reuse the connection's pending-data and finish listeners instead of binding fresh functions per request. Intern HTTP header names in the parser so repeated tokens such as Host and Connection share one V8 string. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: a closed-source coding agent
1 parent dd5dfb5 commit fbe7035

6 files changed

Lines changed: 294 additions & 56 deletions

File tree

lib/_http_incoming.js

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,15 @@ function IncomingMessage(socket) {
6363
return deprecateInstantiation(IncomingMessage, 'DEP0195', socket);
6464
}
6565

66-
let streamOptions;
66+
Readable.call(this);
6767

6868
if (socket) {
69-
streamOptions = {
70-
highWaterMark: socket.readableHighWaterMark,
71-
};
69+
const hwm = socket.readableHighWaterMark;
70+
if (this._readableState.highWaterMark !== hwm) {
71+
this._readableState.highWaterMark = hwm;
72+
}
7273
}
7374

74-
Readable.call(this, streamOptions);
75-
7675
this._readableState.readingMore = true;
7776

7877
this.socket = socket;
@@ -530,6 +529,70 @@ IncomingMessage.prototype._dump = function _dump() {
530529
}
531530
};
532531

532+
// Case-insensitive ASCII compare against an already-lowercased name.
533+
// Avoids allocating a lowercased copy of every header name.
534+
function asciiEqualIgnoreCase(a, lower) {
535+
const len = lower.length;
536+
if (a.length !== len)
537+
return false;
538+
if (a === lower)
539+
return true;
540+
for (let i = 0; i < len; i++) {
541+
let c = a.charCodeAt(i);
542+
if (c >= 65 && c <= 90)
543+
c += 32;
544+
if (c !== lower.charCodeAt(i))
545+
return false;
546+
}
547+
return true;
548+
}
549+
550+
function hasRawHeader(msg, lowerName) {
551+
const rawHeaders = msg.rawHeaders;
552+
const count = msg[kHeadersCount];
553+
for (let i = 0; i < count; i += 2) {
554+
if (asciiEqualIgnoreCase(rawHeaders[i], lowerName))
555+
return true;
556+
}
557+
return false;
558+
}
559+
560+
function getRawHeader(msg, lowerName, joinDuplicates) {
561+
const rawHeaders = msg.rawHeaders;
562+
const count = msg[kHeadersCount];
563+
let result;
564+
for (let i = 0; i < count; i += 2) {
565+
if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
566+
continue;
567+
const value = rawHeaders[i + 1];
568+
if (result === undefined) {
569+
result = value;
570+
if (!joinDuplicates)
571+
return result;
572+
} else {
573+
result += ', ' + value;
574+
}
575+
}
576+
return result;
577+
}
578+
579+
function hasBodyHeaders(msg) {
580+
const rawHeaders = msg.rawHeaders;
581+
const count = msg[kHeadersCount];
582+
for (let i = 0; i < count; i += 2) {
583+
const key = rawHeaders[i];
584+
const length = key.length;
585+
if (length === 14) {
586+
if (asciiEqualIgnoreCase(key, 'content-length'))
587+
return true;
588+
} else if (length === 17) {
589+
if (asciiEqualIgnoreCase(key, 'transfer-encoding'))
590+
return true;
591+
}
592+
}
593+
return false;
594+
}
595+
533596
function onError(self, error, cb) {
534597
// This is to keep backward compatible behavior.
535598
// An error is emitted only if there are listeners attached to the event.
@@ -543,6 +606,9 @@ function onError(self, error, cb) {
543606
module.exports = {
544607
IncomingMessage,
545608
kDetachAbortSignal,
609+
hasRawHeader,
610+
getRawHeader,
611+
hasBodyHeaders,
546612
readStart,
547613
readStop,
548614
};

lib/_http_outgoing.js

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const { getDefaultHighWaterMark } = require('internal/streams/state');
3838
const assert = require('internal/assert');
3939
const EE = require('events');
4040
const Stream = require('stream');
41-
const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http');
41+
const { kOutHeaders, utcDateHeader, kNeedDrain } = require('internal/http');
4242
const { Buffer } = require('buffer');
4343
const {
4444
_checkIsHttpToken: checkIsHttpToken,
@@ -89,6 +89,11 @@ const kChunkedLength = Symbol('kChunkedLength');
8989
const kUniqueHeaders = Symbol('kUniqueHeaders');
9090
const kBytesWritten = Symbol('kBytesWritten');
9191
const kErrored = Symbol('errored');
92+
const kLenientCache = Symbol('kLenientCache');
93+
94+
let keepAliveTimeoutCache = -1;
95+
let keepAliveMaxCache = -1;
96+
let keepAliveHeaderCache = '';
9297
const kWritableFinished = Symbol('kWritableFinished');
9398
const kEndCallbacks = Symbol('kEndCallbacks');
9499
const kFlushError = Symbol('kFlushError');
@@ -169,6 +174,7 @@ function OutgoingMessage(options) {
169174
this[kFlushError] = null;
170175
this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
171176
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
177+
this[kLenientCache] = null;
172178
}
173179
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
174180
ObjectSetPrototypeOf(OutgoingMessage, Stream);
@@ -178,27 +184,34 @@ ObjectSetPrototypeOf(OutgoingMessage, Stream);
178184
// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
179185
// Falls back to global --insecure-http-parser flag.
180186
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
187+
// Options cannot change for the lifetime of a message after headers
188+
// are stored: compute the lookup chain only once.
189+
this[kLenientCache] ??= isLenientHeaderValidation(this);
190+
return this[kLenientCache];
191+
};
192+
193+
function isLenientHeaderValidation(msg) {
181194
// New httpValidation option takes priority (ClientRequest case)
182-
if (this.httpValidation !== undefined) {
183-
return this.httpValidation !== 'strict';
195+
if (msg.httpValidation !== undefined) {
196+
return msg.httpValidation !== 'strict';
184197
}
185198
// ServerResponse: check server's httpValidation option
186-
const serverHttpValidation = this.req?.socket?.server?.httpValidation;
199+
const serverHttpValidation = msg.req?.socket?.server?.httpValidation;
187200
if (serverHttpValidation !== undefined) {
188201
return serverHttpValidation !== 'strict';
189202
}
190203
// Legacy insecureHTTPParser - ClientRequest has it directly
191-
if (typeof this.insecureHTTPParser === 'boolean') {
192-
return this.insecureHTTPParser;
204+
if (typeof msg.insecureHTTPParser === 'boolean') {
205+
return msg.insecureHTTPParser;
193206
}
194207
// ServerResponse can access via req.socket.server
195-
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
208+
const serverOption = msg.req?.socket?.server?.insecureHTTPParser;
196209
if (typeof serverOption === 'boolean') {
197210
return serverOption;
198211
}
199212
// Fall back to global option
200213
return isLenient();
201-
};
214+
}
202215

203216
ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
204217
__proto__: null,
@@ -472,7 +485,7 @@ function _storeHeader(firstLine, headers) {
472485
trailer: false,
473486
header: firstLine,
474487
};
475-
const lenient = this._isLenientHeaderValidation();
488+
const lenient = headers ? this._isLenientHeaderValidation() : false;
476489

477490
if (headers) {
478491
if (headers === this[kOutHeaders]) {
@@ -508,7 +521,7 @@ function _storeHeader(firstLine, headers) {
508521

509522
// Date header
510523
if (this.sendDate && !state.date) {
511-
header += 'Date: ' + utcDate() + '\r\n';
524+
header += utcDateHeader();
512525
}
513526

514527
// Force the connection to close when the response is a 204 No Content or
@@ -541,14 +554,21 @@ function _storeHeader(firstLine, headers) {
541554
if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) {
542555
header += 'Connection: close\r\n';
543556
} else if (shouldSendKeepAlive) {
544-
header += 'Connection: keep-alive\r\n';
545557
if (this._keepAliveTimeout && this._defaultKeepAlive) {
546-
const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000);
547-
let max = '';
548-
if (~~this._maxRequestsPerSocket > 0) {
549-
max = `, max=${this._maxRequestsPerSocket}`;
558+
// Identical for every response of a given server: cache the last
559+
// rendered Connection + Keep-Alive pair.
560+
const timeout = this._keepAliveTimeout;
561+
const max = ~~this._maxRequestsPerSocket;
562+
if (timeout !== keepAliveTimeoutCache || max !== keepAliveMaxCache) {
563+
keepAliveTimeoutCache = timeout;
564+
keepAliveMaxCache = max;
565+
keepAliveHeaderCache = 'Connection: keep-alive\r\n' +
566+
`Keep-Alive: timeout=${MathFloor(timeout / 1000)}` +
567+
(max > 0 ? `, max=${max}` : '') + '\r\n';
550568
}
551-
header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`;
569+
header += keepAliveHeaderCache;
570+
} else {
571+
header += 'Connection: keep-alive\r\n';
552572
}
553573
} else {
554574
this._last = true;
@@ -639,10 +659,38 @@ function storeHeader(self, state, key, value, validate, lenient) {
639659
matchHeader(self, state, key, value);
640660
}
641661

662+
function lowerOutgoingHeaderName(field) {
663+
switch (field) {
664+
case 'Connection':
665+
case 'connection':
666+
return 'connection';
667+
case 'Content-Length':
668+
case 'content-length':
669+
return 'content-length';
670+
case 'Transfer-Encoding':
671+
case 'transfer-encoding':
672+
return 'transfer-encoding';
673+
case 'Date':
674+
case 'date':
675+
return 'date';
676+
case 'Expect':
677+
case 'expect':
678+
return 'expect';
679+
case 'Trailer':
680+
case 'trailer':
681+
return 'trailer';
682+
case 'Keep-Alive':
683+
case 'keep-alive':
684+
return 'keep-alive';
685+
default:
686+
return field.toLowerCase();
687+
}
688+
}
689+
642690
function matchHeader(self, state, field, value) {
643691
if (field.length < 4 || field.length > 17)
644692
return;
645-
field = field.toLowerCase();
693+
field = lowerOutgoingHeaderName(field);
646694
switch (field) {
647695
case 'connection':
648696
state.connection = true;

0 commit comments

Comments
 (0)