http,http2: speed up header validation and write coalescing by anonrig · Pull Request #65332 · nodejs/node · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
http: address review feedback for packed headers
Write packed incoming-header integers as little-endian so s390x and
AIX agree with JS. Export the layout constants from C++ instead of
duplicating them. Keep IncomingMessage.rawHeaders as an own property
to avoid a semver-major prototype change. Queue end() callbacks
before the combined write so sync socket writes still succeed. Drop
the HTTP/2 toLowerCase micro-optimizations and the unused
unpackHeaderList export.

Co-authored-by: Yagiz Nizipli <anonrig@users.noreply.github.com>
  • Loading branch information
cursoragent and anonrig committed Aug 17, 2026
commit 1663db2d5d799942bec667a3d0113312b4d6cb36
8 changes: 0 additions & 8 deletions lib/_http_common.js
90 changes: 51 additions & 39 deletions lib/_http_incoming.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@ const {
deprecateInstantiation,
} = require('internal/util');
const {
HTTPParser,
nativeHeadersHas,
nativeHeadersGet,
nativeHeadersToArray,
} = internalBinding('http_parser');

const kNativeFlagHost = 1 << 0;
const kNativeFlagExpect = 1 << 1;
const kNativeFlagContentLength = 1 << 2;
const kNativeFlagTransferEncoding = 1 << 3;
const kNativeFlagTE = 1 << 4;
const kNativeFlagHost = HTTPParser.kNativeHeaderFlagHost;
const kNativeFlagExpect = HTTPParser.kNativeHeaderFlagExpect;
const kNativeFlagContentLength = HTTPParser.kNativeHeaderFlagContentLength;
const kNativeFlagTransferEncoding =
HTTPParser.kNativeHeaderFlagTransferEncoding;
const kNativeFlagTE = HTTPParser.kNativeHeaderFlagTE;
const kNativeHeadersCountOffset = HTTPParser.kNativeHeadersCountOffset;
const kNativeHeadersFlagsOffset = HTTPParser.kNativeHeadersFlagsOffset;

const { AbortController } = require('internal/abort_controller');

Expand All @@ -51,7 +55,6 @@ const kHeadersDistinct = Symbol('kHeadersDistinct');
const kHeadersCount = Symbol('kHeadersCount');
const kNativeHeaders = Symbol('kNativeHeaders');
const kNativeFlags = Symbol('kNativeFlags');
const kRawHeaders = Symbol('kRawHeaders');
const kTrailers = Symbol('kTrailers');
const kTrailersDistinct = Symbol('kTrailersDistinct');
const kTrailersCount = Symbol('kTrailersCount');
Expand Down Expand Up @@ -100,7 +103,7 @@ function IncomingMessage(socket) {
this[kHeadersCount] = 0;
this[kNativeHeaders] = null;
this[kNativeFlags] = 0;
this[kRawHeaders] = null;
this.rawHeaders = [];
this[kTrailers] = null;
this[kTrailersCount] = 0;
this.rawTrailers = [];
Expand Down Expand Up @@ -140,31 +143,43 @@ ObjectDefineProperty(IncomingMessage.prototype, 'connection', {
},
});

ObjectDefineProperty(IncomingMessage.prototype, 'rawHeaders', {
__proto__: null,
configurable: true,
enumerable: true,
get: function() {
if (this[kRawHeaders] === null) {
if (this[kNativeHeaders] !== null) {
this[kRawHeaders] = nativeHeadersToArray(
this[kNativeHeaders],
this[kHeadersCount],
);
this[kNativeHeaders] = null;
this[kNativeFlags] = 0;
} else {
this[kRawHeaders] = [];
}
}
return this[kRawHeaders];
},
set: function(val) {
this[kRawHeaders] = val;
this[kNativeHeaders] = null;
this[kNativeFlags] = 0;
},
});
function materializeRawHeaders() {
const headers = this[kNativeHeaders] !== null ?
nativeHeadersToArray(this[kNativeHeaders], this[kHeadersCount]) :
[];
this[kNativeHeaders] = null;
this[kNativeFlags] = 0;
ObjectDefineProperty(this, 'rawHeaders', {
__proto__: null,
configurable: true,
enumerable: true,
writable: true,
value: headers,
});
return headers;
}

function setRawHeaders(val) {
this[kNativeHeaders] = null;
this[kNativeFlags] = 0;
ObjectDefineProperty(this, 'rawHeaders', {
__proto__: null,
configurable: true,
enumerable: true,
writable: true,
value: val,
});
}

function defineLazyRawHeaders(self) {
ObjectDefineProperty(self, 'rawHeaders', {
__proto__: null,
configurable: true,
enumerable: true,
get: materializeRawHeaders,
set: setRawHeaders,
});
}

ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
__proto__: null,
Expand Down Expand Up @@ -356,13 +371,11 @@ function abortSignal(self) {

IncomingMessage.prototype._setNativeHeaders = function _setNativeHeaders(native, maxPairs) {
this[kNativeHeaders] = native;
this[kRawHeaders] = null;
this[kNativeFlags] = native[8] | (native[9] << 8) |
(native[10] << 16) | (native[11] << 24);
const pairs = native[4] | (native[5] << 8) |
(native[6] << 16) | (native[7] << 24);
this[kNativeFlags] = native.readUInt32LE(kNativeHeadersFlagsOffset);
const pairs = native.readUInt32LE(kNativeHeadersCountOffset);
const len = pairs * 2;
this[kHeadersCount] = maxPairs > 0 ? MathMin(len, maxPairs) : len;
defineLazyRawHeaders(this);
};

IncomingMessage.prototype._hasHeader = function _hasHeader(name) {
Expand Down Expand Up @@ -609,12 +622,11 @@ IncomingMessage.prototype._hasBodyHeaders = function _hasBodyHeaders() {
const len = name.length;
if (len === 14) {
if (name === 'Content-Length' || name === 'content-length' ||
name === 'CONTENT-LENGTH' || name.toLowerCase() === 'content-length') {
name.toLowerCase() === 'content-length') {
return true;
}
} else if (len === 17) {
if (name === 'Transfer-Encoding' || name === 'transfer-encoding' ||
name === 'TRANSFER-ENCODING' ||
name.toLowerCase() === 'transfer-encoding') {
return true;
}
Expand Down
23 changes: 15 additions & 8 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,11 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
return this;
}

// Queue before write_(): combined chunked-end may call onFinish()
// synchronously (e.g. assignSocket() + custom Writable).
if (typeof callback === 'function')
queueEndCallback(this, callback);

if (this[kSocket]) {
this[kSocket].cork();
}
Expand All @@ -1279,18 +1284,20 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
queueEndCallback(this, callback);
}
return this;
} else if (!this._header) {
if (this[kSocket]) {
this[kSocket].cork();
} else {
if (!this._header) {
if (this[kSocket]) {
this[kSocket].cork();
}

this._contentLength = 0;
this._implicitHeader();
}

this._contentLength = 0;
this._implicitHeader();
if (typeof callback === 'function')
queueEndCallback(this, callback);
}

if (typeof callback === 'function')
queueEndCallback(this, callback);

if (strictContentLength(this) && this[kBytesWritten] !== this._contentLength) {
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength);
}
Expand Down
10 changes: 1 addition & 9 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2858,15 +2858,7 @@ function prepareResponseHeadersArray(headers, options) {
let isDateSet = false;

for (let i = 0; i < headers.length; i += 2) {
let header = headers[i];
const headerLen = header.length;
for (let j = 0; j < headerLen; j++) {
const c = header.charCodeAt(j);
if (c >= 65 && c <= 90) {
header = header.toLowerCase();
break;
}
}
const header = headers[i].toLowerCase();
const value = headers[i + 1];

if (header === HTTP2_HEADER_STATUS) {
Expand Down
12 changes: 1 addition & 11 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,18 +765,8 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
* header validation.
* @returns {[string, number]}
*/
function asciiToLowerIfNeeded(key) {
const keyLen = key.length;
for (let i = 0; i < keyLen; i++) {
const c = key.charCodeAt(i);
if (c >= 65 && c <= 90)
return key.toLowerCase();
}
return key;
}

function processNgHeader(state, key, value) {
key = asciiToLowerIfNeeded(key);
key = key.toLowerCase();
const isSingleValueField = kSingleValueFields.has(key);
const isStrictSingleValueField = state.strictSingleValueFields &&
isSingleValueField;
Expand Down
70 changes: 53 additions & 17 deletions src/node_http_parser.cc
Loading