diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 7694fe4b5b3f3e..862dcc1f6b95be 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -32,6 +32,7 @@ const { ObjectValues, SafeSet, Symbol, + Uint32Array, } = primordials; const { getDefaultHighWaterMark } = require('internal/streams/state'); @@ -74,11 +75,16 @@ const { validateString } = require('internal/validators'); const { assignFunctionName } = require('internal/util'); const { isUint8Array } = require('internal/util/types'); +const { + buildHttpMessage, +} = internalBinding('http_parser'); + let debug = require('internal/util/debuglog').debuglog('http', (fn) => { debug = fn; }); const kCorked = Symbol('corked'); +const kLenientValidation = Symbol('kLenientValidation'); const kSocket = Symbol('kSocket'); const kChunkedBuffer = Symbol('kChunkedBuffer'); const kChunkedLength = Symbol('kChunkedLength'); @@ -92,6 +98,55 @@ const nop = () => {}; const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i; +// Flags passed as args[4] to native buildHttpMessage(). Keep in sync with +// BuildHttpMessageFlags in src/node_http_parser.cc. Combined with | so C++ can +// decide which automatic headers (Date, Connection, Content-Length / TE) to +// emit without re-reading OutgoingMessage fields one-by-one. +// kBuildSendDate - inject Date if the user did not set it +// kBuildShouldKeepAlive - prefer Connection: keep-alive when valid +// kBuildMaxRequestsReached - force Connection: close (server max) +// kBuildDefaultKeepAlive - also emit Keep-Alive: timeout=... +// kBuildHasBody - message may carry a body (not HEAD/204/304) +// kBuildUseChunkedByDefault - allow auto Content-Length / TE: chunked +// kBuildRemovedConnection - user called removeHeader('connection') +// kBuildRemovedContLen - user called removeHeader('content-length') +// kBuildRemovedTE - user called removeHeader('transfer-encoding') +// kBuildHasAgent - ClientRequest has an agent (keep-alive only; +// does NOT enable auto Content-Length) +const kBuildSendDate = 1 << 0; +const kBuildShouldKeepAlive = 1 << 1; +const kBuildMaxRequestsReached = 1 << 2; +const kBuildDefaultKeepAlive = 1 << 3; +const kBuildHasBody = 1 << 4; +const kBuildUseChunkedByDefault = 1 << 5; +const kBuildRemovedConnection = 1 << 6; +const kBuildRemovedContLen = 1 << 7; +const kBuildRemovedTE = 1 << 8; +const kBuildHasAgent = 1 << 9; + +// Output slots written by buildHttpMessage into args[9] (Uint32Array). +// Keep in sync with BuildHttpMessageOut in src/node_http_parser.cc. +// [kOutLast] - 1 if this is the last message on the connection +// [kOutChunked] - 1 if Transfer-Encoding: chunked was selected +// [kOutContentLength] - resolved body length when known +// [kOutHasContentLength] - 1 if kOutContentLength is valid +const kOutLast = 0; +const kOutChunked = 1; +const kOutContentLength = 2; +const kOutHasContentLength = 3; +const kOutCount = 4; + +// Scratch buffer for native out-params. Reused so each response does not +// allocate a new Uint32Array; only the four slots above are written/read. +const buildHttpMessageOut = new Uint32Array(kOutCount); + +// Lazy to avoid a require cycle with _http_server. +let _statusCodes; +function statusCodes() { + _statusCodes ??= require('_http_server').STATUS_CODES; + return _statusCodes; +} + // isCookieField performs a case-insensitive comparison of a provided string // against the word "cookie." As of V8 6.6 this is faster than handrolling or // using a case-insensitive RegExp. @@ -143,6 +198,7 @@ function OutgoingMessage(options) { this[kChunkedBuffer] = []; this[kChunkedLength] = 0; this._closed = false; + this[kLenientValidation] = undefined; this[kSocket] = null; this._header = null; @@ -163,28 +219,39 @@ ObjectSetPrototypeOf(OutgoingMessage, Stream); // 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. +// The answer is invariant for the lifetime of the message (the options are +// fixed at construction time), but this runs on every setHeader/appendHeader +// call and once per stored header block, so the multi-step lookup chain is +// resolved once per message and cached. OutgoingMessage.prototype._isLenientHeaderValidation = function() { + const cached = this[kLenientValidation]; + if (cached !== undefined) + return cached; + return this[kLenientValidation] = lenientHeaderValidation(this); +}; + +function lenientHeaderValidation(msg) { // New httpValidation option takes priority (ClientRequest case) - if (this.httpValidation !== undefined) { - return this.httpValidation !== 'strict'; + if (msg.httpValidation !== undefined) { + return msg.httpValidation !== 'strict'; } // ServerResponse: check server's httpValidation option - const serverHttpValidation = this.req?.socket?.server?.httpValidation; + const serverHttpValidation = msg.req?.socket?.server?.httpValidation; if (serverHttpValidation !== undefined) { return serverHttpValidation !== 'strict'; } // Legacy insecureHTTPParser - ClientRequest has it directly - if (typeof this.insecureHTTPParser === 'boolean') { - return this.insecureHTTPParser; + if (typeof msg.insecureHTTPParser === 'boolean') { + return msg.insecureHTTPParser; } // ServerResponse can access via req.socket.server - const serverOption = this.req?.socket?.server?.insecureHTTPParser; + const serverOption = msg.req?.socket?.server?.insecureHTTPParser; if (typeof serverOption === 'boolean') { return serverOption; } // Fall back to global option return isLenient(); -}; +} ObjectDefineProperty(OutgoingMessage.prototype, 'errored', { __proto__: null, @@ -315,11 +382,12 @@ OutgoingMessage.prototype.uncork = function uncork() { callbacks.push(buf[n + 2]); } } - this._send(crlf_buf, null, callbacks.length ? (err) => { - for (const callback of callbacks) { - callback(err); - } - } : null); + // The runner is a shared top-level function so flushing a corked + // chunked buffer does not allocate a fresh closure environment per + // flush. + this._send(crlf_buf, null, callbacks?.length ? + runChunkCallbacks.bind(undefined, callbacks) : + null); this[kChunkedBuffer].length = 0; this[kChunkedLength] = 0; @@ -376,26 +444,89 @@ function emitDestroyNT(self) { } +// Queue an unsent header block ahead of the body write. Used for Buffer +// headers (native builder) and for string headers that cannot share the +// body's encoding (high-byte latin1 / non-utf8 body encodings). +function queueHeaderWrite(msg, header, encoding) { + msg.outputData.unshift({ + data: header, + encoding, + callback: null, + }); + const len = header.length; + msg.outputSize += len; + msg._onPendingData(len); +} + // This abstract either writing directly to the socket or buffering it. OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteLength) { // This is a shameful hack to get the headers and first body chunk onto // the same packet. Future versions of Node are going to take care of // this at a lower level and in a more general way. - if (!this._headerSent && this._header !== null) { + let header; + if (!this._headerSent && (header = this._header) !== null) { // `this._header` can be null if OutgoingMessage is used without a proper Socket // See: /test/parallel/test-http-outgoing-message-inheritance.js - if (typeof data === 'string' && - (encoding === 'utf8' || encoding === 'latin1' || !encoding)) { - data = this._header + data; + // + // Native builder stores _header as a Buffer (server path). Legacy and + // ClientRequest keep a latin1 string. Never Buffer.concat: queue the + // header as its own write when it cannot share the body encoding. + if (isUint8Array(header)) { + // Native Buffer header. Prefer a single write when the body encoding + // can share the stream; otherwise queue the header separately. + if (typeof data === 'string' && data.length === 0) { + data = header; + encoding = 'buffer'; + } else if (typeof data === 'string' && data.length > 0 && + (encoding === 'latin1' || encoding === 'binary')) { + data = header.toString('latin1') + data; + encoding = 'latin1'; + } else if (typeof data === 'string' && data.length > 0 && + (encoding === 'utf8' || encoding === 'utf-8' || !encoding)) { + let ascii = true; + for (let i = 0; i < header.byteLength; i++) { + if (header[i] > 127) { + ascii = false; + break; + } + } + if (ascii) { + // latin1 decode of ASCII is identity; one utf8 write with body. + data = header.toString('latin1') + data; + } else { + queueHeaderWrite(this, header, 'buffer'); + } + } else if (isUint8Array(data) && data.byteLength > 0) { + queueHeaderWrite(this, header, 'buffer'); + } else { + queueHeaderWrite(this, header, 'buffer'); + } + } else if (typeof data === 'string' && data.length > 0 && + (encoding === 'latin1' || encoding === 'binary')) { + data = header + data; + encoding = 'latin1'; + } else if (typeof data === 'string' && data.length > 0 && + (encoding === 'utf8' || encoding === 'utf-8' || !encoding)) { + // ASCII headers can safely share one UTF-8 write with the body. + let ascii = true; + for (let i = 0; i < header.length; i++) { + if (header.charCodeAt(i) > 127) { + ascii = false; + break; + } + } + if (ascii) { + data = header + data; + } else { + queueHeaderWrite(this, header, 'latin1'); + } + } else if (typeof data === 'string' && data.length === 0) { + // Header-only flush (end without body, Expect: 100-continue, etc.). + data = header; + encoding = 'latin1'; } else { - const header = this._header; - this.outputData.unshift({ - data: header, - encoding: 'latin1', - callback: null, - }); - this.outputSize += header.length; - this._onPendingData(header.length); + // Buffers / hex / utf16le / base64: separate header write. + queueHeaderWrite(this, header, 'latin1'); } this._headerSent = true; } @@ -434,6 +565,211 @@ function _writeRaw(data, encoding, callback, size) { OutgoingMessage.prototype._storeHeader = _storeHeader; function _storeHeader(firstLine, headers) { + // Prefer the C++ single-buffer builder. Falls back to the legacy JS path + // only when headers need JS-only special handling (content-disposition + // latin1, unique-header cookie joining, etc.). + if (tryNativeStoreHeader(this, firstLine, headers)) { + return; + } + legacyStoreHeader(this, firstLine, headers); +} + +function buildFlags(msg) { + let flags = 0; + if (msg.sendDate) flags |= kBuildSendDate; + if (msg.shouldKeepAlive) flags |= kBuildShouldKeepAlive; + if (msg.maxRequestsOnConnectionReached) flags |= kBuildMaxRequestsReached; + if (msg._defaultKeepAlive) flags |= kBuildDefaultKeepAlive; + if (msg._hasBody) flags |= kBuildHasBody; + // useChunkedEncodingByDefault and agent are independent: agent only + // participates in keep-alive decisions, not Content-Length emission. + if (msg.useChunkedEncodingByDefault) flags |= kBuildUseChunkedByDefault; + if (msg._removedConnection) flags |= kBuildRemovedConnection; + if (msg._removedContLen) flags |= kBuildRemovedContLen; + if (msg._removedTE) flags |= kBuildRemovedTE; + if (msg.agent) flags |= kBuildHasAgent; + return flags; +} + +function applyBuildResult(msg, out) { + if (out[kOutLast]) msg._last = true; + if (out[kOutChunked]) msg.chunkedEncoding = true; + else msg.chunkedEncoding = false; + if (out[kOutHasContentLength]) + msg._contentLength = out[kOutContentLength]; +} + +// Flatten headers into [name, value, ...] for the C++ builder. Returns null +// when a header requires the legacy JS path. +function flattenHeadersForNative(msg, headers, validate) { + const flat = flatHeadersScratch; + flat.length = 0; + const lenient = msg._isLenientHeaderValidation(); + + function pushPair(key, value, doValidate) { + if (doValidate) { + validateHeaderName(key); + } + // content-disposition + content-length needs latin1 Buffer values - JS only. + if (isContentDispositionField(key) && msg._contentLength) { + return false; + } + if (ArrayIsArray(value)) { + const valueLength = value.length; + if ( + (valueLength < 2 || !isCookieField(key)) && + (!msg[kUniqueHeaders] || !msg[kUniqueHeaders].has(key.toLowerCase())) + ) { + for (let i = 0; i < valueLength; i++) { + if (doValidate) validateHeaderValue(key, value[i], lenient); + // Buffers not supported on the native path. + if (typeof value[i] !== 'string') return false; + flat.push(key, value[i]); + } + return true; + } + value = value.join('; '); + } + if (doValidate) validateHeaderValue(key, value, lenient); + if (typeof value !== 'string') return false; + flat.push(key, value); + return true; + } + + if (!headers) return flat; + + if (headers === msg[kOutHeaders]) { + for (const key in headers) { + const entry = headers[key]; + if (!pushPair(entry[0], entry[1], false)) return null; + } + return flat; + } + + if (ArrayIsArray(headers)) { + const headersLength = headers.length; + if (headersLength && ArrayIsArray(headers[0])) { + for (let i = 0; i < headersLength; i++) { + const entry = headers[i]; + if (!pushPair(entry[0], entry[1], validate)) return null; + } + } else { + if (headersLength % 2 !== 0) { + throw new ERR_INVALID_ARG_VALUE('headers', headers); + } + for (let n = 0; n < headersLength; n += 2) { + if (!pushPair(headers[n], headers[n + 1], validate)) return null; + } + } + return flat; + } + + for (const key in headers) { + if (ObjectHasOwn(headers, key)) { + if (!pushPair(key, headers[key], validate)) return null; + } + } + return flat; +} + +function headerListHasChunkedTE(flat) { + for (let i = 0; i < flat.length; i += 2) { + const name = flat[i]; + if (name.length === 17 && name.toLowerCase() === 'transfer-encoding') { + if (RE_TE_CHUNKED.test(flat[i + 1])) return true; + } + } + return false; +} + +function tryNativeStoreHeader(msg, firstLine, headers) { + const flat = flattenHeadersForNative( + msg, headers, headers !== msg[kOutHeaders]); + if (flat === null) return false; + + // Force the connection to close when the response is a 204 No Content or + // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" + // header. Must run before building so Connection: close is emitted. + // RFC 2616: 204/304 MUST NOT have a body; keep TE on the wire for compat + // but do not actually chunk-encode, and close the connection. + const noBodyStatus = msg.statusCode === 204 || msg.statusCode === 304; + if (noBodyStatus && (msg.chunkedEncoding || headerListHasChunkedTE(flat))) { + debug(msg.statusCode + ' response should not use chunked encoding,' + + ' closing connection.'); + msg.chunkedEncoding = false; + msg.shouldKeepAlive = false; + } + + const knownLength = + typeof msg._contentLength === 'number' ? msg._contentLength : -1; + const keepAliveSec = msg._keepAliveTimeout ? + MathFloor(msg._keepAliveTimeout / 1000) : + 0; + const maxReq = msg._maxRequestsPerSocket | 0; + const date = msg.sendDate ? utcDate() : ''; + + buildHttpMessageOut[0] = 0; + buildHttpMessageOut[1] = 0; + buildHttpMessageOut[2] = 0; + buildHttpMessageOut[3] = 0; + + const buf = buildHttpMessage( + firstLine, + flat, + null, + 0, + buildFlags(msg), + date, + keepAliveSec, + maxReq, + knownLength, + buildHttpMessageOut, + ); + if (!buf) return false; + + applyBuildResult(msg, buildHttpMessageOut); + + if (noBodyStatus) { + msg.chunkedEncoding = false; + } + + // Trailer without chunked is invalid (same check as legacy path). + // The C++ builder does not throw; detect trailer presence cheaply. + for (let i = 0; i < flat.length; i += 2) { + const name = flat[i]; + if (name.length === 7 && (name === 'Trailer' || name === 'trailer' || + name.toLowerCase() === 'trailer') && !msg.chunkedEncoding) { + throw new ERR_HTTP_TRAILER_INVALID(); + } + } + + // ServerResponse: keep the Buffer to avoid a latin1 string copy on the + // hot writeHead()+end() path. ClientRequest: materialize a latin1 string + // so userland/tests that treat _header as a string (e.g. .match) keep + // working. + if (typeof msg.statusCode === 'number') { + msg._header = buf; + } else { + msg._header = buf.toString('latin1'); + } + msg._headerSent = false; + + // Expect: 100-continue forces an early header flush (legacy behavior). + for (let i = 0; i < flat.length; i += 2) { + const name = flat[i]; + if (name.length === 6 && (name === 'Expect' || name === 'expect' || + name.toLowerCase() === 'expect')) { + msg._send(''); + break; + } + } + return true; +} + +// Reused flat header list for native builds (sync only; reset each call). +const flatHeadersScratch = []; + +function legacyStoreHeader(self, firstLine, headers) { // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' // in the case of response it is: 'HTTP/1.1 200 OK\r\n' const state = { @@ -445,33 +781,37 @@ function _storeHeader(firstLine, headers) { trailer: false, header: firstLine, }; - const lenient = this._isLenientHeaderValidation(); + const lenient = self._isLenientHeaderValidation(); if (headers) { - if (headers === this[kOutHeaders]) { + if (headers === self[kOutHeaders]) { for (const key in headers) { const entry = headers[key]; - processHeader(this, state, entry[0], entry[1], false, lenient); + processHeader(self, state, entry[0], entry[1], false, lenient); } } else if (ArrayIsArray(headers)) { - if (headers.length && ArrayIsArray(headers[0])) { - for (let i = 0; i < headers.length; i++) { + // The length is hoisted into a local because the engine cannot fold + // the reload across the processHeader calls; the array is never + // mutated while this loop runs. + const headersLength = headers.length; + if (headersLength && ArrayIsArray(headers[0])) { + for (let i = 0; i < headersLength; i++) { const entry = headers[i]; - processHeader(this, state, entry[0], entry[1], true, lenient); + processHeader(self, state, entry[0], entry[1], true, lenient); } } else { - if (headers.length % 2 !== 0) { + if (headersLength % 2 !== 0) { throw new ERR_INVALID_ARG_VALUE('headers', headers); } - for (let n = 0; n < headers.length; n += 2) { - processHeader(this, state, headers[n + 0], headers[n + 1], true, lenient); + for (let n = 0; n < headersLength; n += 2) { + processHeader(self, state, headers[n + 0], headers[n + 1], true, lenient); } } } else { for (const key in headers) { if (ObjectHasOwn(headers, key)) { - processHeader(this, state, key, headers[key], true, lenient); + processHeader(self, state, key, headers[key], true, lenient); } } } @@ -480,7 +820,7 @@ function _storeHeader(firstLine, headers) { let { header } = state; // Date header - if (this.sendDate && !state.date) { + if (self.sendDate && !state.date) { header += 'Date: ' + utcDate() + '\r\n'; } @@ -495,53 +835,56 @@ function _storeHeader(firstLine, headers) { // It was pointed out that this might confuse reverse proxies to the point // of creating security liabilities, so suppress the zero chunk and force // the connection to close. - if (this.chunkedEncoding && (this.statusCode === 204 || - this.statusCode === 304)) { - debug(this.statusCode + ' response should not use chunked encoding,' + + if (self.chunkedEncoding && (self.statusCode === 204 || + self.statusCode === 304)) { + debug(self.statusCode + ' response should not use chunked encoding,' + ' closing connection.'); - this.chunkedEncoding = false; - this.shouldKeepAlive = false; + self.chunkedEncoding = false; + self.shouldKeepAlive = false; } // keep-alive logic - if (this._removedConnection) { + if (self._removedConnection) { // shouldKeepAlive is generally true for HTTP/1.1. In that common case, // even if the connection header isn't sent, we still persist by default. - this._last = !this.shouldKeepAlive; + self._last = !self.shouldKeepAlive; } else if (!state.connection) { - const shouldSendKeepAlive = this.shouldKeepAlive && - (state.contLen || this.useChunkedEncodingByDefault || this.agent); - if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) { + const shouldSendKeepAlive = self.shouldKeepAlive && + (state.contLen || self.useChunkedEncodingByDefault || self.agent); + if (shouldSendKeepAlive && self.maxRequestsOnConnectionReached) { header += 'Connection: close\r\n'; } else if (shouldSendKeepAlive) { header += 'Connection: keep-alive\r\n'; - if (this._keepAliveTimeout && this._defaultKeepAlive) { - const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000); + const keepAliveTimeout = self._keepAliveTimeout; + if (keepAliveTimeout && self._defaultKeepAlive) { + const timeoutSeconds = MathFloor(keepAliveTimeout / 1000); + const maxRequestsPerSocket = self._maxRequestsPerSocket; let max = ''; - if (~~this._maxRequestsPerSocket > 0) { - max = `, max=${this._maxRequestsPerSocket}`; + if (~~maxRequestsPerSocket > 0) { + max = `, max=${maxRequestsPerSocket}`; } header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`; } } else { - this._last = true; + self._last = true; header += 'Connection: close\r\n'; } } + let contentLength; if (!state.contLen && !state.te) { - if (!this._hasBody) { + if (!self._hasBody) { // Make sure we don't end the 0\r\n\r\n at the end of the message. - this.chunkedEncoding = false; - } else if (!this.useChunkedEncodingByDefault) { - this._last = true; + self.chunkedEncoding = false; + } else if (!self.useChunkedEncodingByDefault) { + self._last = true; } else if (!state.trailer && - !this._removedContLen && - typeof this._contentLength === 'number') { - header += 'Content-Length: ' + this._contentLength + '\r\n'; - } else if (!this._removedTE) { + !self._removedContLen && + typeof (contentLength = self._contentLength) === 'number') { + header += 'Content-Length: ' + contentLength + '\r\n'; + } else if (!self._removedTE) { header += 'Transfer-Encoding: chunked\r\n'; - this.chunkedEncoding = true; + self.chunkedEncoding = true; } else { // We should only be able to get here if both Content-Length and // Transfer-Encoding are removed by the user. @@ -550,7 +893,7 @@ function _storeHeader(firstLine, headers) { // We can't keep alive in this case, because with no header info the body // is defined as all data until the connection is closed. - this._last = true; + self._last = true; } } @@ -558,16 +901,16 @@ function _storeHeader(firstLine, headers) { // message will be terminated by the first empty line after the // header fields, regardless of the header fields present in the // message, and thus cannot contain a message body or 'trailers'. - if (this.chunkedEncoding !== true && state.trailer) { + if (self.chunkedEncoding !== true && state.trailer) { throw new ERR_HTTP_TRAILER_INVALID(); } - this._header = header + '\r\n'; - this._headerSent = false; + self._header = header + '\r\n'; + self._headerSent = false; // Wait until the first body chunk, or close(), is sent to flush, // UNLESS we're sending Expect: 100-continue. - if (state.expect) this._send(''); + if (state.expect) self._send(''); } function processHeader(self, state, key, value, validate, lenient) { @@ -590,13 +933,14 @@ function processHeader(self, state, key, value, validate, lenient) { } if (ArrayIsArray(value)) { + const valueLength = value.length; if ( - (value.length < 2 || !isCookieField(key)) && + (valueLength < 2 || !isCookieField(key)) && (!self[kUniqueHeaders] || !self[kUniqueHeaders].has(key.toLowerCase())) ) { // Retain for(;;) loop for performance reasons // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < value.length; i++) + for (let i = 0; i < valueLength; i++) storeHeader(self, state, key, value[i], validate, lenient); return; } @@ -613,8 +957,23 @@ function storeHeader(self, state, key, value, validate, lenient) { } function matchHeader(self, state, field, value) { - if (field.length < 4 || field.length > 17) - return; + const len = field.length; + // Cheap (length, first character) pre-filter so the common case, a + // header that is not one of the eight known fields below, returns + // without paying the per-header toLowerCase() allocation. The filter + // only rejects names that cannot possibly match the switch: `| 0x20` + // lower-cases ASCII letters and maps no other token character onto a + // letter, and every known field length is enumerated. + const c = field.charCodeAt(0) | 0x20; + switch (len) { + case 4: if (c !== 0x64) return; break; // Date + case 6: if (c !== 0x65) return; break; // Expect + case 7: if (c !== 0x74) return; break; // Trailer + case 10: if (c !== 0x63 && c !== 0x6b) return; break; // Connection, Keep-Alive + case 14: if (c !== 0x63) return; break; // Content-Length + case 17: if (c !== 0x74) return; break; // Transfer-Encoding + default: return; // No known field has this length + } field = field.toLowerCase(); switch (field) { case 'connection': @@ -997,9 +1356,12 @@ function write_(msg, chunk, encoding, callback, fromEnd) { } } - if (!fromEnd && msg.socket && !msg.socket.writableCorked) { - msg.socket.cork(); - process.nextTick(connectionCorkNT, msg.socket); + // `socket` is an accessor on the prototype: load it once instead of + // paying the getter three times on every body write. + let socket; + if (!fromEnd && (socket = msg.socket) && !socket.writableCorked) { + socket.cork(); + process.nextTick(connectionCorkNT, socket); } let ret; @@ -1028,6 +1390,12 @@ function connectionCorkNT(conn) { conn.uncork(); } +function runChunkCallbacks(callbacks, err) { + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](err); + } +} + OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { if (this.finished) { throw new ERR_HTTP_HEADERS_SENT('set trailing'); @@ -1036,6 +1404,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { this._trailer = ''; const keys = ObjectKeys(headers); const isArray = ArrayIsArray(headers); + const lenient = this._isLenientHeaderValidation(); // Retain for(;;) loop for performance reasons // Refs: https://github.com/nodejs/node/pull/30958 for (let i = 0, l = keys.length; i < l; i++) { @@ -1052,7 +1421,6 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { // Check if the field must be sent several times const isArrayValue = ArrayIsArray(value); - const lenient = this._isLenientHeaderValidation(); if ( isArrayValue && value.length > 1 && (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase())) @@ -1093,6 +1461,13 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { encoding = null; } + // Single-shot fast path: headers not yet rendered, socket assigned, one + // body chunk (or empty). Builds the entire HTTP message in C++ and issues + // a single socket.write(). + if (tryFastEnd(this, chunk, encoding, callback)) { + return this; + } + if (chunk) { if (this.finished) { onError(this, @@ -1105,7 +1480,17 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { this[kSocket].cork(); } - write_(this, chunk, encoding, null, true); + try { + write_(this, chunk, encoding, null, true); + } catch (err) { + // write_() can throw on invalid chunk types before any data is + // queued; undo the cork so a subsequent end() is not stuck. + if (this[kSocket]) { + this[kSocket]._writableState.corked = 1; + this[kSocket].uncork(); + } + throw err; + } } else if (this.finished) { if (typeof callback === 'function') { if (!this.writableFinished) { @@ -1163,6 +1548,277 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { return this; }; +function getFastFirstLine(msg) { + // ServerResponse + if (typeof msg.statusCode === 'number') { + const statusCode = msg.statusCode | 0; + if (statusCode < 100 || statusCode > 999) return null; + let statusMessage = msg.statusMessage; + if (statusMessage === undefined || statusMessage === null) { + statusMessage = statusCodes()[statusCode] || 'unknown'; + msg.statusMessage = statusMessage; + } + if (checkInvalidHeaderChar(statusMessage)) { + throw new ERR_INVALID_CHAR('statusMessage'); + } + // Align with writeHead() side effects for informational / no-body codes. + if (statusCode === 204 || statusCode === 304 || + (statusCode >= 100 && statusCode <= 199)) { + msg._hasBody = false; + } + if (msg._expect_continue && !msg._sent100) { + msg.shouldKeepAlive = false; + } + return `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`; + } + // ClientRequest + if (typeof msg.method === 'string' && typeof msg.path === 'string') { + return msg.method + ' ' + msg.path + ' HTTP/1.1\r\n'; + } + return null; +} + +// Common status lines (writeHead / fast end without custom reason). +const kStatusLine200 = 'HTTP/1.1 200 OK\r\n'; + +function prepareFastBody(msg, chunk, encoding) { + let body = null; + let bodyEncoding = 0; // 0 = buffer/none, 1 = latin1, 2 = utf8 + let bodyLen = 0; + if (chunk) { + if (typeof chunk === 'string') { + if (encoding && encoding !== 'utf8' && encoding !== 'utf-8' && + encoding !== 'latin1' && encoding !== 'binary' && + encoding !== 'ascii') { + return null; + } + body = chunk; + if (encoding === 'latin1' || encoding === 'binary' || + encoding === 'ascii') { + bodyEncoding = 1; + bodyLen = chunk.length; + } else { + bodyEncoding = 2; + bodyLen = Buffer.byteLength(chunk, 'utf8'); + } + } else if (isUint8Array(chunk)) { + body = chunk; + bodyEncoding = 0; + bodyLen = chunk.byteLength; + } else { + return null; + } + } + + if (!msg._hasBody && body && bodyLen > 0) { + if (msg[kRejectNonStandardBodyWrites]) { + throw new ERR_HTTP_BODY_NOT_ALLOWED(); + } + body = null; + bodyLen = 0; + } + return { body, bodyEncoding, bodyLen }; +} + +function finishFastSocketWrite(msg, socket, ret) { + if (!ret) msg[kNeedDrain] = true; + debug('outgoing message fast end.'); + if (msg.outputData.length === 0 && socket._httpMessage === msg) { + msg._finish(); + } +} + +// writeHead() already rendered headers: flush header + body without write_(). +// This is the benchmark/http/simple.js path. +function tryFastFlushEnd(msg, chunk, encoding, callback) { + if (!msg._header || msg._headerSent || msg.finished || msg.destroyed) + return false; + if (msg.chunkedEncoding || msg._trailer) return false; + if (msg.outputData.length !== 0 || msg[kCorked]) return false; + if (msg._expect_continue && !msg._sent100) return false; + if (strictContentLength(msg)) return false; + + const socket = msg[kSocket]; + if (!socket || socket._httpMessage !== msg || !socket.writable || + socket.destroyed || !socket._writableState || !socket._handle) { + return false; + } + + const prepared = prepareFastBody(msg, chunk, encoding); + if (prepared === null) return false; + const { body, bodyEncoding, bodyLen } = prepared; + + if (typeof callback === 'function') + msg.once('finish', callback); + + const finish = onFinish.bind(undefined, msg); + const header = msg._header; + + if (!socket.writableCorked) + socket.cork(); + + let ret; + if (isUint8Array(header)) { + // Native Buffer header: one or two writes under cork (no string copy). + if (body && bodyLen > 0 && msg._hasBody) { + socket.write(header); + if (bodyEncoding === 0) { + ret = socket.write(body, finish); + } else if (bodyEncoding === 1) { + ret = socket.write(body, 'latin1', finish); + } else { + ret = socket.write(body, 'utf8', finish); + } + } else { + ret = socket.write(header, finish); + } + } else if (body && bodyLen > 0 && msg._hasBody) { + if (typeof body === 'string' && bodyEncoding === 1) { + ret = socket.write(header + body, 'latin1', finish); + } else if (typeof body === 'string') { + // utf8 body: only concat when the header is pure ASCII. + let ascii = true; + for (let i = 0; i < header.length; i++) { + if (header.charCodeAt(i) > 127) { + ascii = false; + break; + } + } + if (ascii) { + ret = socket.write(header + body, finish); + } else { + socket.write(header, 'latin1'); + ret = socket.write(body, 'utf8', finish); + } + } else { + socket.write(header, 'latin1'); + ret = socket.write(body, finish); + } + } else { + ret = socket.write(header, 'latin1', finish); + } + + socket._writableState.corked = 1; + socket.uncork(); + + msg._headerSent = true; + msg.finished = true; + if (bodyLen && msg._hasBody) + msg[kBytesWritten] = bodyLen; + + finishFastSocketWrite(msg, socket, ret); + return true; +} + +function tryFastEnd(msg, chunk, encoding, callback) { + // ServerResponse only. ClientRequest has method-specific Content-Length + // rules (GET/HEAD/DELETE must not emit CL:0) and CONNECT tunnel framing + // that the C++ builder does not model. + if (typeof msg.statusCode !== 'number') + return false; + + if (msg.finished || msg.destroyed) + return false; + + // Hot path after writeHead(): headers already built, not yet flushed. + if (msg._header && !msg._headerSent) { + return tryFastFlushEnd(msg, chunk, encoding, callback); + } + + // Full native single-shot: nothing rendered yet. + if (msg._header || msg._headerSent) + return false; + if (msg._trailer) return false; + if (msg.outputData.length !== 0) return false; + if (msg[kCorked]) return false; + if (msg._expect_continue && !msg._sent100) return false; + + const socket = msg[kSocket]; + // Require a real net.Socket (has a libuv _handle). Standalone + // ServerResponse tests assign arbitrary Writables that must keep the + // multi-write _send() behaviour. + if (!socket || socket._httpMessage !== msg || !socket.writable || + socket.destroyed || !socket._writableState || !socket._handle) { + return false; + } + + const prepared = prepareFastBody(msg, chunk, encoding); + if (prepared === null) return false; + const { body, bodyEncoding, bodyLen } = prepared; + + let firstLine; + if (msg.statusCode === 200 && + (msg.statusMessage === undefined || msg.statusMessage === null || + msg.statusMessage === 'OK')) { + msg.statusMessage = 'OK'; + firstLine = kStatusLine200; + } else { + firstLine = getFastFirstLine(msg); + if (firstLine === null) return false; + } + + const flat = flattenHeadersForNative(msg, msg[kOutHeaders], false); + if (flat === null) return false; + + if (body && msg._hasBody) { + msg._contentLength = bodyLen; + } else { + msg._contentLength = 0; + } + + const knownLength = + typeof msg._contentLength === 'number' ? msg._contentLength : -1; + const keepAliveSec = msg._keepAliveTimeout ? + MathFloor(msg._keepAliveTimeout / 1000) : + 0; + const maxReq = msg._maxRequestsPerSocket | 0; + const date = msg.sendDate ? utcDate() : ''; + + const buf = buildHttpMessage( + firstLine, + flat, + body, + bodyEncoding, + buildFlags(msg), + date, + keepAliveSec, + maxReq, + knownLength, + buildHttpMessageOut, + ); + if (!buf) return false; + + applyBuildResult(msg, buildHttpMessageOut); + + if (msg.chunkedEncoding && (msg.statusCode === 204 || + msg.statusCode === 304)) { + return false; + } + + if (typeof callback === 'function') + msg.once('finish', callback); + + // Keep Buffer form for consistency with tryNativeStoreHeader (server). + msg._header = buf; + msg._headerSent = true; + msg.finished = true; + if (bodyLen && msg._hasBody) { + msg[kBytesWritten] = bodyLen; + } + + const finish = onFinish.bind(undefined, msg); + + if (!socket.writableCorked) { + socket.cork(); + } + const ret = socket.write(buf, finish); + socket._writableState.corked = 1; + socket.uncork(); + + finishFastSocketWrite(msg, socket, ret); + return true; +} + // This function is called once all user data are flushed to the socket. // Note that it has a chance that the socket is not drained. diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index d99b61780fad6c..950e1e97ab61f5 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -31,9 +31,11 @@ #include "stream_base-inl.h" #include "v8.h" +#include // snprintf #include // free() #include // strdup(), strchr() - +#include +#include // This is a binding to llhttp (https://github.com/nodejs/llhttp) // The goal is to decouple sockets from parsing for more javascript-level @@ -70,6 +72,7 @@ using v8::Object; using v8::ObjectTemplate; using v8::String; using v8::Uint32; +using v8::Uint32Array; using v8::Undefined; using v8::Value; @@ -1293,6 +1296,456 @@ const llhttp_settings_t Parser::settings = { nullptr, }; +// Flags for BuildHttpMessage - keep in sync with lib/_http_outgoing.js. +enum BuildHttpMessageFlags : uint32_t { + kBuildSendDate = 1u << 0, + kBuildShouldKeepAlive = 1u << 1, + kBuildMaxRequestsReached = 1u << 2, + kBuildDefaultKeepAlive = 1u << 3, + kBuildHasBody = 1u << 4, + kBuildUseChunkedByDefault = 1u << 5, + kBuildRemovedConnection = 1u << 6, + kBuildRemovedContLen = 1u << 7, + kBuildRemovedTE = 1u << 8, + kBuildHasAgent = 1u << 9, +}; + +// Out indices written into the optional Uint32Array passed as args[8]. +enum BuildHttpMessageOut : uint32_t { + kOutLast = 0, + kOutChunked = 1, + kOutContentLength = 2, + kOutHasContentLength = 3, + kOutCount = 4, +}; + +static bool HeaderTokenEquals(const char* a, + size_t a_len, + const char* b, + size_t b_len) { + if (a_len != b_len) return false; + for (size_t i = 0; i < a_len; i++) { + char ca = a[i]; + char cb = b[i]; + if (ca >= 'A' && ca <= 'Z') ca = static_cast(ca + 32); + if (cb >= 'A' && cb <= 'Z') cb = static_cast(cb + 32); + if (ca != cb) return false; + } + return true; +} + +static bool ValueContainsTokenCI(const char* value, + size_t len, + const char* token, + size_t token_len) { + if (token_len == 0 || len < token_len) return false; + for (size_t i = 0; i + token_len <= len; i++) { + bool match = true; + for (size_t j = 0; j < token_len; j++) { + char c = value[i + j]; + if (c >= 'A' && c <= 'Z') c = static_cast(c + 32); + if (c != token[j]) { + match = false; + break; + } + } + if (!match) continue; + const bool left_ok = i == 0 || value[i - 1] == ' ' || value[i - 1] == ',' || + value[i - 1] == '\t'; + const bool right_ok = i + token_len == len || value[i + token_len] == ' ' || + value[i + token_len] == ',' || + value[i + token_len] == '\t'; + if (left_ok && right_ok) return true; + } + return false; +} + +// Build a complete HTTP/1.1 message (header block, optionally plus body) in a +// single contiguous Buffer. Mirrors _storeHeader() keep-alive / length logic +// so the JS fast path can skip intermediate string concatenation. +// +// args[0] firstLine String e.g. "HTTP/1.1 200 OK\r\n" +// args[1] headers Array|null flat [name, value, name, value, ...] +// args[2] body String|Uint8Array|null +// args[3] bodyEncoding 0=none/buffer, 1=latin1, 2=utf8 (only for string body) +// args[4] flags uint32 BuildHttpMessageFlags +// args[5] date String (pre-formatted UTC date) or empty +// args[6] keepAliveSec uint32 keep-alive timeout in seconds +// args[7] maxRequests int32 max requests per socket (0 = omit) +// args[8] knownLength int32 content-length if already known, else -1 +// args[9] out Uint32Array(kOutCount) optional result flags +void BuildHttpMessage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + + if (args.Length() < 9 || !args[0]->IsString()) { + return; + } + + Local first_line_v = args[0].As(); + + const uint32_t flags = + args[4]->IsUint32() ? args[4].As()->Value() : 0; + const bool send_date = (flags & kBuildSendDate) != 0; + const bool should_keep_alive = (flags & kBuildShouldKeepAlive) != 0; + const bool max_requests_reached = (flags & kBuildMaxRequestsReached) != 0; + const bool default_keep_alive = (flags & kBuildDefaultKeepAlive) != 0; + const bool has_body = (flags & kBuildHasBody) != 0; + const bool use_chunked_by_default = (flags & kBuildUseChunkedByDefault) != 0; + const bool removed_connection = (flags & kBuildRemovedConnection) != 0; + const bool removed_cont_len = (flags & kBuildRemovedContLen) != 0; + const bool removed_te = (flags & kBuildRemovedTE) != 0; + const bool has_agent = (flags & kBuildHasAgent) != 0; + + int64_t known_length = -1; + if (args[8]->IsNumber()) { + known_length = args[8]->IntegerValue(env->context()).FromMaybe(-1); + } + + // Measure / extract body. + size_t body_len = 0; + bool body_is_buffer = false; + bool body_is_string = false; + int body_encoding = 0; // 0=buffer/none, 1=latin1, 2=utf8 + Local body_str; + const char* body_buf_data = nullptr; + + if (!args[2]->IsNullOrUndefined()) { + if (Buffer::HasInstance(args[2])) { + body_is_buffer = true; + body_buf_data = Buffer::Data(args[2]); + body_len = Buffer::Length(args[2]); + } else if (args[2]->IsString()) { + body_is_string = true; + body_str = args[2].As(); + body_encoding = args[3]->IsUint32() ? args[3].As()->Value() : 2; + if (body_encoding == 1) { + body_len = static_cast(body_str->Length()); + } else { + body_len = static_cast(body_str->Utf8LengthV2(isolate)); + body_encoding = 2; + } + } + } + + bool saw_connection = false; + bool saw_cont_len = false; + bool saw_te = false; + bool saw_date = false; + bool saw_trailer = false; + bool saw_keep_alive = false; + bool connection_close = false; + bool te_chunked = false; + int64_t header_content_length = -1; + + std::vector, Local>> header_pairs; + if (args[1]->IsArray()) { + Local headers = args[1].As(); + const uint32_t len = headers->Length(); + if (len % 2u != 0u) { + return; // Invalid; JS validates before calling. + } + header_pairs.reserve(len / 2); + Local context = env->context(); + for (uint32_t i = 0; i < len; i += 2) { + Local k_v, v_v; + if (!headers->Get(context, i).ToLocal(&k_v) || + !headers->Get(context, i + 1).ToLocal(&v_v) || !k_v->IsString() || + !v_v->IsString()) { + return; + } + Local key = k_v.As(); + Local val = v_v.As(); + header_pairs.emplace_back(key, val); + + const int klen = key->Length(); + const int vlen = val->Length(); + + // Cheap length pre-filter matching matchHeader() in JS. + if (klen != 4 && klen != 6 && klen != 7 && klen != 10 && klen != 14 && + klen != 17) { + continue; + } + + char name_buf[32]; + if (klen < 0 || klen >= static_cast(sizeof(name_buf))) continue; + key->WriteOneByteV2( + isolate, 0, klen, reinterpret_cast(name_buf)); + + char value_buf_stack[128]; + std::string value_heap; + const char* value_ptr; + size_t value_len = static_cast(vlen); + if (vlen < static_cast(sizeof(value_buf_stack))) { + val->WriteOneByteV2( + isolate, 0, vlen, reinterpret_cast(value_buf_stack)); + value_ptr = value_buf_stack; + } else { + value_heap.resize(value_len); + val->WriteOneByteV2( + isolate, 0, vlen, reinterpret_cast(&value_heap[0])); + value_ptr = value_heap.data(); + } + + if (HeaderTokenEquals(name_buf, klen, "date", 4)) { + saw_date = true; + } else if (HeaderTokenEquals(name_buf, klen, "trailer", 7)) { + saw_trailer = true; + } else if (HeaderTokenEquals(name_buf, klen, "connection", 10)) { + saw_connection = true; + if (ValueContainsTokenCI(value_ptr, value_len, "close", 5)) + connection_close = true; + } else if (HeaderTokenEquals(name_buf, klen, "keep-alive", 10)) { + saw_keep_alive = true; + } else if (HeaderTokenEquals(name_buf, klen, "content-length", 14)) { + saw_cont_len = true; + header_content_length = 0; + for (size_t j = 0; j < value_len; j++) { + if (value_ptr[j] < '0' || value_ptr[j] > '9') break; + header_content_length = + header_content_length * 10 + (value_ptr[j] - '0'); + } + } else if (HeaderTokenEquals(name_buf, klen, "transfer-encoding", 17)) { + saw_te = true; + if (ValueContainsTokenCI(value_ptr, value_len, "chunked", 7)) + te_chunked = true; + } + } + } + + bool is_last = false; + bool chunked = te_chunked; + int64_t content_length = -1; + + if (saw_cont_len) { + content_length = header_content_length; + } else if (known_length >= 0) { + content_length = known_length; + } else if (body_is_buffer || body_is_string) { + content_length = static_cast(body_len); + } + + // Match _storeHeader connection logic. + // JS: shouldKeepAlive && (contLen || useChunkedByDefault || agent) + bool emit_connection_close = false; + bool emit_connection_keep_alive = false; + if (removed_connection) { + is_last = !should_keep_alive; + } else if (!saw_connection) { + const bool should_send_keep_alive = + should_keep_alive && (saw_cont_len || content_length >= 0 || + use_chunked_by_default || has_agent); + if (should_send_keep_alive && max_requests_reached) { + emit_connection_close = true; + } else if (should_send_keep_alive) { + emit_connection_keep_alive = true; + } else { + is_last = true; + emit_connection_close = true; + } + } else if (connection_close) { + is_last = true; + } + + // Body framing. Note: useChunkedByDefault alone gates Content-Length / + // Transfer-Encoding auto headers - agent is NOT involved here. + bool need_auto_cont_len = false; + bool need_auto_te = false; + if (!saw_cont_len && !saw_te) { + if (!has_body) { + chunked = false; + } else if (!use_chunked_by_default) { + is_last = true; + } else if (!saw_trailer && !removed_cont_len && content_length >= 0) { + need_auto_cont_len = true; + } else if (!removed_te) { + need_auto_te = true; + chunked = true; + } else { + is_last = true; + } + } + + // Single-shot body: prefer Content-Length over chunked when the message + // actually uses chunked-by-default (servers / POST/PUT clients). + if ((body_is_buffer || body_is_string) && content_length >= 0 && !saw_te && + !removed_cont_len && use_chunked_by_default) { + need_auto_cont_len = !saw_cont_len; + need_auto_te = false; + chunked = false; + } + + // statusCode is not passed in; 204/304 + chunked is handled by JS before + // or after calling this builder (see tryNativeStoreHeader). + + // body arg: null/undefined => headers-only (_storeHeader); string/buffer + // (possibly empty) => complete message. Headers-only must not append the + // chunked terminator - JS end() still does that. + const bool headers_only = args[2]->IsNullOrUndefined(); + + // Size estimate (upper bound) so we can write into a single malloc and + // hand it to Buffer without a second copy. + size_t est = static_cast(first_line_v->Length()) + 128 + body_len; + for (const auto& pair : header_pairs) { + est += static_cast(pair.first->Length()) + + static_cast(pair.second->Length()) + 4; // ": \r\n" + } + if (chunked) est += 32; // chunk framing + + char* raw = UncheckedMalloc(est); + if (raw == nullptr) { + return; + } + size_t off = 0; + + auto append_raw = [&](const char* p, size_t n) { + CHECK_LE(off + n, est); + if (n > 0) { + std::memcpy(raw + off, p, n); + off += n; + } + }; + auto append_lit = [&](const char* lit) { append_raw(lit, std::strlen(lit)); }; + auto append_v8_latin1 = [&](Local s) { + const int n = s->Length(); + CHECK_LE(off + static_cast(n), est); + if (s->IsOneByte()) { + s->WriteOneByteV2(isolate, 0, n, reinterpret_cast(raw + off)); + } else { + std::vector tmp(static_cast(n)); + s->WriteV2(isolate, 0, n, tmp.data()); + for (int i = 0; i < n; i++) { + raw[off + static_cast(i)] = + static_cast(tmp[static_cast(i)] & 0xff); + } + } + off += static_cast(n); + }; + auto append_number = [&](uint64_t n) { + char buf[32]; + size_t i = sizeof(buf); + do { + buf[--i] = static_cast('0' + (n % 10)); + n /= 10; + } while (n > 0); + append_raw(buf + i, sizeof(buf) - i); + }; + + append_v8_latin1(first_line_v); + + for (const auto& pair : header_pairs) { + append_v8_latin1(pair.first); + append_lit(": "); + append_v8_latin1(pair.second); + append_lit("\r\n"); + } + + if (send_date && !saw_date && args[5]->IsString()) { + append_lit("Date: "); + append_v8_latin1(args[5].As()); + append_lit("\r\n"); + } + + if (emit_connection_keep_alive) { + append_lit("Connection: keep-alive\r\n"); + uint32_t ka = 0; + if (args[6]->IsNumber()) { + ka = args[6]->Uint32Value(env->context()).FromMaybe(0); + } + if (default_keep_alive && !saw_keep_alive && ka > 0) { + append_lit("Keep-Alive: timeout="); + append_number(ka); + int32_t max_req = 0; + if (args[7]->IsNumber()) { + max_req = args[7]->Int32Value(env->context()).FromMaybe(0); + } + if (max_req > 0) { + append_lit(", max="); + append_number(static_cast(max_req)); + } + append_lit("\r\n"); + } + } else if (emit_connection_close) { + append_lit("Connection: close\r\n"); + } + + if (need_auto_cont_len && content_length >= 0) { + append_lit("Content-Length: "); + append_number(static_cast(content_length)); + append_lit("\r\n"); + } + if (need_auto_te) { + append_lit("Transfer-Encoding: chunked\r\n"); + } + + append_lit("\r\n"); + + if (!headers_only && has_body && (body_is_buffer || body_is_string)) { + if (chunked && body_len > 0) { + char hex[16]; + const int n = + std::snprintf(hex, + sizeof(hex), + "%llx", + static_cast(body_len)); // NOLINT + append_raw(hex, static_cast(n)); + append_lit("\r\n"); + } + if (body_is_buffer && body_len > 0) { + append_raw(body_buf_data, body_len); + } else if (body_is_string && body_len > 0) { + CHECK_LE(off + body_len + 1, est); + if (body_encoding == 1) { + body_str->WriteOneByteV2(isolate, + 0, + body_str->Length(), + reinterpret_cast(raw + off)); + off += body_len; + } else { + const size_t written = body_str->WriteUtf8V2( + isolate, raw + off, body_len + 1, String::WriteFlags::kNone); + size_t payload = written; + if (payload > 0 && raw[off + payload - 1] == '\0') payload--; + off += payload; + } + } + if (chunked) { + if (body_len > 0) append_lit("\r\n"); + append_lit("0\r\n\r\n"); + } + } + + Local buf_obj; + // Transfer ownership of `raw` to the Buffer; free callback releases it. + if (!Buffer::New( + isolate, + raw, + off, + [](char* data, void* /*hint*/) { free(data); }, + nullptr) + .ToLocal(&buf_obj)) { + free(raw); + return; + } + + if (args.Length() > 9 && args[9]->IsUint32Array()) { + Local out_arr = args[9].As(); + if (out_arr->Length() >= kOutCount) { + auto* data = + static_cast(out_arr->Buffer()->GetBackingStore()->Data()); + data += out_arr->ByteOffset() / sizeof(uint32_t); + data[kOutLast] = is_last ? 1 : 0; + data[kOutChunked] = chunked ? 1 : 0; + data[kOutContentLength] = + content_length >= 0 ? static_cast(content_length) : 0; + data[kOutHasContentLength] = content_length >= 0 ? 1 : 0; + } + } + + args.GetReturnValue().Set(buf_obj); +} + void CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Isolate* isolate = isolate_data->isolate(); @@ -1370,6 +1823,9 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, SetConstructorFunction(isolate, target, "HTTPParser", t); + // Single-shot HTTP/1.1 message builder used by the OutgoingMessage fast path. + SetMethod(isolate, target, "buildHttpMessage", BuildHttpMessage); + Local c = NewFunctionTemplate(isolate, ConnectionsList::New); c->InstanceTemplate() @@ -1436,6 +1892,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(Parser::Consume); registry->Register(Parser::Unconsume); registry->Register(Parser::GetCurrentBuffer); + registry->Register(BuildHttpMessage); registry->Register(ConnectionsList::New); registry->Register(ConnectionsList::All); registry->Register(ConnectionsList::Idle);