Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bb74093
node:http: take two per-request costs off the server hot path
cirospaciari Jul 8, 2026
31fa271
node:http: one shared finish listener instead of three bind() closure…
cirospaciari Jul 8, 2026
7c7e407
node:http: materialize request headers lazily and dispatch on a nativ…
cirospaciari Jul 10, 2026
4653756
node:http: trim native crossings on the response lifecycle
cirospaciari Jul 10, 2026
db68ab6
node:http: remove per-request allocations from the response hot path
cirospaciari Jul 10, 2026
f6ffb76
node:http: writeHeadAndEnd - one native call for the writeHead+end pair
cirospaciari Jul 10, 2026
272cfe9
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 10, 2026
2add3e7
node:http: hoist end()'s nextTick callback to a named function
cirospaciari Jul 10, 2026
f24db98
node:http: drop expressions the dispatch bitfield made unused, hoist …
cirospaciari Jul 10, 2026
243507d
node:http: write the framework response headers natively from cached …
cirospaciari Jul 10, 2026
b1ed562
node:http: address review findings and CI failures
cirospaciari Jul 10, 2026
e5c027e
node:http: declare the keep-alive idle-start socket field, make rawHe…
cirospaciari Jul 10, 2026
1f63df2
node:http: update two comments the fix commits made stale
cirospaciari Jul 10, 2026
c0c92fa
node:http: harden writeHeadAndEnd error paths and the keep-alive time…
cirospaciari Jul 10, 2026
d318949
node:http: report writeHead exceptions through the FFI return value, …
cirospaciari Jul 10, 2026
75acc88
node:http: acknowledge the writeHead ThrowScope from the Rust caller
cirospaciari Jul 10, 2026
114c7f2
node:http: keep buildRawHeadersArray's doc comment attached to its de…
cirospaciari Jul 10, 2026
cf0841a
node:http2: honor the auto-header bits in the allowHTTP1 fallback
cirospaciari Jul 10, 2026
0c7d668
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 10, 2026
5663326
node:http: release the scratch header array when a render throws; gat…
cirospaciari Jul 10, 2026
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
7 changes: 6 additions & 1 deletion src/js/internal/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ export const enum NodeHTTPBodyReadState {
export const enum NodeHTTPResponseFlags {
socket_closed = 1 << 0,
request_has_completed = 1 << 1,
ended = 1 << 2,
upgraded = 1 << 3,

closed_or_completed = socket_closed | request_has_completed,
}
Expand Down Expand Up @@ -200,7 +202,10 @@ function emitEOFIncomingMessageOuter(self) {
// parserOnMessageComplete. Native moved the section onto THIS request's
// handle at its body fin, so pipelined requests can neither inherit nor
// overwrite another request's trailers.
if (self[kHandle] !== undefined) {
// Trailers can only follow a chunked request body: a no-body request has
// none, so skip the native call (and the socket/server option walk) for the
// common GET/HEAD case.
if (self[kHandle] !== undefined && !self[noBodySymbol]) {
// The lenient (insecureHTTPParser) value bytes must match what the parser
// accepted on the wire, or a CTL byte in a trailer value would vanish here.
let rawTrailers = self[kHandle].takeRequestTrailers(self.socket?.server?.insecureHTTPParser === true);
Expand Down
75 changes: 54 additions & 21 deletions src/js/node/_http_incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ const ObjectDefineProperty = Object.defineProperty;
const ArrayPrototypeSlice = Array.prototype.slice;

const kHeaders = Symbol("kHeaders");
// Cache slot for the server dispatcher's keep-alive decision (stamped once
// per request in _http_server.ts); declared in the constructor so the stamp
// never shape-transitions the request.
const kReqShouldKeepAlive = Symbol("kReqShouldKeepAlive");
const kHeadersDistinct = Symbol("kHeadersDistinct");
const kHeadersCount = Symbol("kHeadersCount");
// Lazy req.rawHeaders: cache slot + the native handle the bytes live on
// (never cleared - unlike kHandle - so post-_destroy access still works).
const kRawHeaders = Symbol("kRawHeaders");
const kHeaderSource = Symbol("kHeaderSource");
const kTrailers = Symbol("kTrailers");
const kTrailersDistinct = Symbol("kTrailersDistinct");
const kTrailersCount = Symbol("kTrailersCount");
Expand Down Expand Up @@ -74,6 +82,9 @@ function IncomingMessage(socket) {
this.complete = false;
this._closed = false;
this[kHeaders] = null;
this[kReqShouldKeepAlive] = undefined;
this[kRawHeaders] = null;
this[kHeaderSource] = null;
this[kHeadersCount] = 0;
this[kTrailers] = null;
this[kTrailersCount] = 0;
Expand All @@ -85,20 +96,11 @@ function IncomingMessage(socket) {
this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse;
this.url = arguments[1];
this.method = arguments[2];
// `headers` (arguments[3]) is intentionally not used: the lazy `headers`
// getter builds the object from rawHeaders with Node.js's duplicate
// handling (joining, cookie/set-cookie rules, joinDuplicateHeaders),
// which the native object does not implement.
let rawHeaders = arguments[4];
// Node.js's parser keeps at most server.maxHeadersCount header pairs
// (parser.maxHeaderPairs); the native parser does not enforce it, so
// truncate here.
const maxHeadersCount = arguments[7]?.server?.maxHeadersCount;
if (typeof maxHeadersCount === "number" && maxHeadersCount > 0 && rawHeaders.length > maxHeadersCount * 2) {
rawHeaders = ArrayPrototypeSlice.$call(rawHeaders, 0, maxHeadersCount * 2);
}
this.rawHeaders = rawHeaders;
this[kHeadersCount] = rawHeaders.length;
// arguments[3] carries no headers object and arguments[4] no rawHeaders
// on the native fast path anymore: the raw header bytes stay captured on
// the native handle and req.rawHeaders / req.headers are materialized by
// the rawHeaders accessor only when user code reads them.
this[kHeaderSource] = arguments[5];
this[kHandle] = arguments[5];
this[noBodySymbol] = !arguments[6];
this[fakeSocketSymbol] = arguments[7];
Expand Down Expand Up @@ -202,20 +204,51 @@ ObjectDefineProperty(IncomingMessage.prototype, "aborted", {
},
});

// Materializes on first read from the bytes captured on the native handle
// (takeRawHeaders). Assignment (the llhttp client path, or user code)
// short-circuits materialization through the setter.
ObjectDefineProperty(IncomingMessage.prototype, "rawHeaders", {
__proto__: null,
get: function () {
let raw = this[kRawHeaders];
if (raw === null) {
const source = this[kHeaderSource];
let built = source != null ? source.takeRawHeaders() : undefined;
if (built === undefined) built = [];
// Node.js's parser keeps at most server.maxHeadersCount header pairs
// (parser.maxHeaderPairs); the native parser does not enforce it, so
// truncate here.
const maxHeadersCount = this[fakeSocketSymbol]?.server?.maxHeadersCount;
if (typeof maxHeadersCount === "number" && maxHeadersCount > 0 && built.length > maxHeadersCount * 2) {
built = ArrayPrototypeSlice.$call(built, 0, maxHeadersCount * 2);
}
raw = this[kRawHeaders] = built;
this[kHeadersCount] = built.length;
}
return raw;
},
set: function (value) {
this[kRawHeaders] = value;
this[kHeadersCount] = value ? value.length : 0;
},
});
Comment thread
claude[bot] marked this conversation as resolved.
Resolved

ObjectDefineProperty(IncomingMessage.prototype, "headers", {
__proto__: null,
get: function () {
if (!this[kHeaders]) {
this[kHeaders] = {};
let dst = this[kHeaders];
if (!dst) {
dst = this[kHeaders] = {};

const src = this.rawHeaders;
const dst = this[kHeaders];
const count = this[kHeadersCount];
const addHeaderLine = this._addHeaderLine;

for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
for (let n = 0; n < count; n += 2) {
addHeaderLine.$call(this, src[n + 0], src[n + 1], dst);
}
}
return this[kHeaders];
return dst;
},
set: function (val) {
this[kHeaders] = val;
Expand Down Expand Up @@ -644,4 +677,4 @@ function onError(self, error, cb) {
}
}

export { IncomingMessage, readStart, readStop };
export { IncomingMessage, kReqShouldKeepAlive, readStart, readStop };
49 changes: 31 additions & 18 deletions src/js/node/_http_outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const kBytesWritten = Symbol("kBytesWritten");
const kErrored = Symbol("errored");
const kHighWaterMark = Symbol("kHighWaterMark");
const kRejectNonStandardBodyWrites = Symbol("kRejectNonStandardBodyWrites");
const kLenientValidation = Symbol("kLenientValidation");

const nop = () => {};

Expand Down Expand Up @@ -100,36 +101,48 @@ function OutgoingMessage(options) {
this[kErrored] = null;
this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
// Declared here so the memoizing _isLenientHeaderValidation never
// shape-transitions the message when it caches.
this[kLenientValidation] = undefined;
}
$toClass(OutgoingMessage, "OutgoingMessage", Stream);

// Check if lenient header validation should be used.
// For ClientRequest: checks this.httpValidation or this.insecureHTTPParser
// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
// Falls back to global --insecure-http-parser flag.
// Every input is fixed for the life of the message, but setHeader and
// appendHeader consult this once per header: compute once, cache in the
// constructor-declared slot.
OutgoingMessage.prototype._isLenientHeaderValidation = function () {
const cached = this[kLenientValidation];
if (cached !== undefined) return cached;

let result;
// New httpValidation option takes priority (ClientRequest case)
const httpValidation = this.httpValidation;
if (httpValidation !== undefined) {
return httpValidation !== "strict";
}
// ServerResponse: check server's httpValidation option
const serverHttpValidation = this.req?.socket?.server?.httpValidation;
if (serverHttpValidation !== undefined) {
return serverHttpValidation !== "strict";
}
// Legacy insecureHTTPParser - ClientRequest has it directly
const insecureHTTPParser = this.insecureHTTPParser;
if (typeof insecureHTTPParser === "boolean") {
return insecureHTTPParser;
}
// ServerResponse can access via req.socket.server
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
if (typeof serverOption === "boolean") {
return serverOption;
result = httpValidation !== "strict";
} else {
// ServerResponse routes both options through the owning server; walk the
// req -> socket -> server chain once, not once per option.
const server = this.req?.socket?.server;
const serverHttpValidation = server?.httpValidation;
if (serverHttpValidation !== undefined) {
result = serverHttpValidation !== "strict";
} else {
// Legacy insecureHTTPParser - ClientRequest has it directly
const insecureHTTPParser = this.insecureHTTPParser;
if (typeof insecureHTTPParser === "boolean") {
result = insecureHTTPParser;
} else {
// ServerResponse can access via the server
const serverOption = server?.insecureHTTPParser;
result = typeof serverOption === "boolean" ? serverOption : isLenient();
}
}
}
// Fall back to global option
return isLenient();
return (this[kLenientValidation] = result);
};

ObjectDefineProperty(OutgoingMessage.prototype, "errored", {
Expand Down
Loading
Loading