Files
node/lib/_http_outgoing.js
T
Naman Trivedi 0700e749eb http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to
socket-connected writing (Path A), the backpressure domain changes —
subsequent writes go directly to the socket, which enforces its own
backpressure via socket.write() return values.  The OM should emit
drain at this transition point to signal that its buffer is clear and
the caller can resume writing under the socket backpressure regime.

Previously, _flush() gated drain emission on writableLength === 0
which included socket.writableLength.  This conflated two independent
backpressure domains: the OM pre-socket buffer and the socket kernel
write queue.  When the socket had a higher writableHighWaterMark than
the OM (e.g. agent-reused socket from a prior request), the socket
was never backpressured and never emitted drain, causing a permanent
deadlock.

Additionally, avoid reusing a pooled socket in http.Agent when its
writableHighWaterMark differs from the request highWaterMark, so that
the user backpressure threshold is respected for the common case of
the built-in Agent.

Signed-off-by: Naman Trivedi <trivenay@amazon.com>
Fixes: https://github.com/nodejs/node/issues/64680
Refs: https://github.com/nodejs/node/pull/64653
Refs: https://github.com/nodejs/node/pull/62936
PR-URL: https://github.com/nodejs/node/pull/64991
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-08-08 20:43:08 -07:00

1311 lines
37 KiB
JavaScript

// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const {
Array,
ArrayIsArray,
MathFloor,
ObjectDefineProperty,
ObjectHasOwn,
ObjectKeys,
ObjectSetPrototypeOf,
ObjectValues,
SafeSet,
Symbol,
} = primordials;
const { getDefaultHighWaterMark } = require('internal/streams/state');
const assert = require('internal/assert');
const EE = require('events');
const Stream = require('stream');
const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http');
const { Buffer } = require('buffer');
const {
_checkIsHttpToken: checkIsHttpToken,
_checkInvalidHeaderChar: checkInvalidHeaderChar,
chunkExpression: RE_TE_CHUNKED,
isLenient,
} = require('_http_common');
const {
defaultTriggerAsyncIdScope,
symbols: { async_id_symbol },
} = require('internal/async_hooks');
const {
codes: {
ERR_HTTP_BODY_NOT_ALLOWED,
ERR_HTTP_CONTENT_LENGTH_MISMATCH,
ERR_HTTP_HEADERS_SENT,
ERR_HTTP_INVALID_HEADER_VALUE,
ERR_HTTP_TRAILER_INVALID,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_CHAR,
ERR_INVALID_HTTP_TOKEN,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_STREAM_ALREADY_FINISHED,
ERR_STREAM_CANNOT_PIPE,
ERR_STREAM_DESTROYED,
ERR_STREAM_NULL_VALUES,
ERR_STREAM_WRITE_AFTER_END,
},
hideStackFrames,
} = require('internal/errors');
const { validateString } = require('internal/validators');
const {
assignFunctionName,
deprecateInstantiation,
} = require('internal/util');
const { isUint8Array } = require('internal/util/types');
let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
debug = fn;
});
const kCorked = Symbol('corked');
const kSocket = Symbol('kSocket');
const kChunkedBuffer = Symbol('kChunkedBuffer');
const kChunkedLength = Symbol('kChunkedLength');
const kUniqueHeaders = Symbol('kUniqueHeaders');
const kBytesWritten = Symbol('kBytesWritten');
const kErrored = Symbol('errored');
const kWritableFinished = Symbol('kWritableFinished');
const kEndCallbacks = Symbol('kEndCallbacks');
const kFlushError = Symbol('kFlushError');
const kHighWaterMark = Symbol('kHighWaterMark');
const kRejectNonStandardBodyWrites = Symbol('kRejectNonStandardBodyWrites');
const nop = () => {};
const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i;
// 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.
function isCookieField(s) {
return s.length === 6 && s.toLowerCase() === 'cookie';
}
function isContentDispositionField(s) {
return s.length === 19 && s.toLowerCase() === 'content-disposition';
}
function OutgoingMessage(options) {
if (!(this instanceof OutgoingMessage)) {
return deprecateInstantiation(OutgoingMessage, 'DEP0195', options);
}
Stream.call(this);
// Queue that holds all currently pending data, until the response will be
// assigned to the socket (until it will its turn in the HTTP pipeline).
this.outputData = [];
// `outputSize` is an approximate measure of how much data is queued on this
// response. `_onPendingData` will be invoked to update similar global
// per-connection counter. That counter will be used to pause/unpause the
// TCP socket and HTTP Parser and thus handle the backpressure.
this.outputSize = 0;
this.writable = true;
this.destroyed = false;
this._last = false;
this.chunkedEncoding = false;
this.shouldKeepAlive = true;
this.maxRequestsOnConnectionReached = false;
this._defaultKeepAlive = true;
this.useChunkedEncodingByDefault = true;
this.sendDate = false;
this._removedConnection = false;
this._removedContLen = false;
this._removedTE = false;
this.strictContentLength = false;
this[kBytesWritten] = 0;
this._contentLength = null;
this._hasBody = true;
this._trailer = '';
this[kNeedDrain] = false;
this.finished = false;
this._headerSent = false;
this[kCorked] = 0;
this[kChunkedBuffer] = [];
this[kChunkedLength] = 0;
this._closed = false;
this[kSocket] = null;
this._header = null;
this[kOutHeaders] = null;
this._keepAliveTimeout = 0;
this._onPendingData = nop;
this[kErrored] = null;
this[kWritableFinished] = false;
this[kEndCallbacks] = null;
this[kFlushError] = null;
this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
}
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
ObjectSetPrototypeOf(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.
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
// New httpValidation option takes priority (ClientRequest case)
if (this.httpValidation !== undefined) {
return this.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
if (typeof this.insecureHTTPParser === 'boolean') {
return this.insecureHTTPParser;
}
// ServerResponse can access via req.socket.server
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
if (typeof serverOption === 'boolean') {
return serverOption;
}
// Fall back to global option
return isLenient();
};
ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
__proto__: null,
get() {
return this[kErrored];
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'closed', {
__proto__: null,
get() {
return this._closed;
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableFinished', {
__proto__: null,
get() {
return this[kWritableFinished];
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableObjectMode', {
__proto__: null,
get() {
return false;
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableLength', {
__proto__: null,
get() {
return this.outputSize + this[kChunkedLength] + (this[kSocket] ? this[kSocket].writableLength : 0);
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableHighWaterMark', {
__proto__: null,
get() {
return this[kSocket] ? this[kSocket].writableHighWaterMark : this[kHighWaterMark];
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableCorked', {
__proto__: null,
get() {
return this[kCorked];
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'connection', {
__proto__: null,
get: function() {
return this[kSocket];
},
set: function(val) {
this.socket = val;
},
});
ObjectDefineProperty(OutgoingMessage.prototype, 'socket', {
__proto__: null,
get: function() {
return this[kSocket];
},
set: function(val) {
for (let n = 0; n < this[kCorked]; n++) {
val?.cork();
this[kSocket]?.uncork();
}
this[kSocket] = val;
},
});
OutgoingMessage.prototype._renderHeaders = function _renderHeaders() {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('render');
}
const headersMap = this[kOutHeaders];
const headers = {};
if (headersMap !== null) {
const keys = ObjectKeys(headersMap);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
headers[headersMap[key][0]] = headersMap[key][1];
}
}
return headers;
};
OutgoingMessage.prototype.cork = function cork() {
this[kCorked]++;
if (this[kSocket]) {
this[kSocket].cork();
}
};
OutgoingMessage.prototype.uncork = function uncork() {
this[kCorked]--;
if (this[kSocket]) {
this[kSocket].uncork();
}
if (this[kCorked] || this[kChunkedBuffer].length === 0) {
return;
}
const len = this[kChunkedLength];
const buf = this[kChunkedBuffer];
assert(this.chunkedEncoding);
let callbacks;
this._send(len.toString(16), 'latin1', null);
this._send(crlf_buf, null, null);
for (let n = 0; n < buf.length; n += 3) {
this._send(buf[n + 0], buf[n + 1], null);
if (buf[n + 2]) {
callbacks ??= [];
callbacks.push(buf[n + 2]);
}
}
this._send(crlf_buf, null, callbacks.length ? (err) => {
for (const callback of callbacks) {
callback(err);
}
} : null);
this[kChunkedBuffer].length = 0;
this[kChunkedLength] = 0;
// If we had a pending drain and flushed all data, emit the drain event.
if (this[kNeedDrain] && this.writableLength === 0) {
this[kNeedDrain] = false;
this.emit('drain');
}
};
OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
if (callback) {
this.on('timeout', callback);
}
if (!this[kSocket]) {
this.once('socket', function socketSetTimeoutOnConnect(socket) {
socket.setTimeout(msecs);
});
} else {
this[kSocket].setTimeout(msecs);
}
return this;
};
// It's possible that the socket will be destroyed, and removed from
// any messages, before ever calling this. In that case, just skip
// it, since something else is destroying this connection anyway.
OutgoingMessage.prototype.destroy = function destroy(error) {
if (this.destroyed) {
return this;
}
this.destroyed = true;
this[kErrored] = error;
if (this[kSocket]) {
this[kSocket].destroy(error);
} else {
process.nextTick(emitDestroyNT, this);
}
return this;
};
function emitDestroyNT(self) {
if (!self._closed) {
self._closed = true;
self.emit('close');
}
}
// 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) {
// `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;
} else {
const header = this._header;
this.outputData.unshift({
data: header,
encoding: 'latin1',
callback: null,
});
this.outputSize += header.length;
this._onPendingData(header.length);
}
this._headerSent = true;
}
return this._writeRaw(data, encoding, callback, byteLength);
};
OutgoingMessage.prototype._writeRaw = _writeRaw;
function _writeRaw(data, encoding, callback, size) {
const conn = this[kSocket];
if (conn?.destroyed) {
// The socket was destroyed. If we're still trying to write to it,
// then we haven't gotten the 'close' event yet.
return false;
}
if (typeof encoding === 'function') {
callback = encoding;
encoding = null;
}
if (conn && conn._httpMessage === this && conn.writable) {
// There might be pending data in the this.output buffer.
if (this.outputData.length) {
this._flushOutput(conn);
}
// Directly write to socket.
return conn.write(data, encoding, callback);
}
// Buffer, as long as we're not destroyed.
this.outputData.push({ data, encoding, callback });
this.outputSize += data.length;
this._onPendingData(data.length);
return this.outputSize < this[kHighWaterMark];
}
OutgoingMessage.prototype._storeHeader = _storeHeader;
function _storeHeader(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 = {
connection: false,
contLen: false,
te: false,
date: false,
expect: false,
trailer: false,
header: firstLine,
};
const lenient = this._isLenientHeaderValidation();
if (headers) {
if (headers === this[kOutHeaders]) {
for (const key in headers) {
const entry = headers[key];
processHeader(this, 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++) {
const entry = headers[i];
processHeader(this, state, entry[0], entry[1], true, lenient);
}
} else {
if (headers.length % 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);
}
}
} else {
for (const key in headers) {
if (ObjectHasOwn(headers, key)) {
processHeader(this, state, key, headers[key], true, lenient);
}
}
}
}
let { header } = state;
// Date header
if (this.sendDate && !state.date) {
header += 'Date: ' + utcDate() + '\r\n';
}
// 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.
//
// RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but
// node.js used to send out a zero chunk anyway to accommodate clients
// that don't have special handling for those responses.
//
// 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,' +
' closing connection.');
this.chunkedEncoding = false;
this.shouldKeepAlive = false;
}
// keep-alive logic
if (this._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;
} else if (!state.connection) {
const shouldSendKeepAlive = this.shouldKeepAlive &&
(state.contLen || this.useChunkedEncodingByDefault || this.agent);
if (shouldSendKeepAlive && this.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);
let max = '';
if (~~this._maxRequestsPerSocket > 0) {
max = `, max=${this._maxRequestsPerSocket}`;
}
header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`;
}
} else {
this._last = true;
header += 'Connection: close\r\n';
}
}
if (!state.contLen && !state.te) {
if (!this._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;
} else if (!state.trailer &&
!this._removedContLen &&
typeof this._contentLength === 'number') {
header += 'Content-Length: ' + this._contentLength + '\r\n';
} else if (!this._removedTE) {
header += 'Transfer-Encoding: chunked\r\n';
this.chunkedEncoding = true;
} else {
// We should only be able to get here if both Content-Length and
// Transfer-Encoding are removed by the user.
// See: test/parallel/test-http-remove-header-stays-removed.js
debug('Both Content-Length and Transfer-Encoding are removed');
// 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;
}
}
// Test non-chunked message does not have trailer header set,
// 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) {
throw new ERR_HTTP_TRAILER_INVALID();
}
this._header = header + '\r\n';
this._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('');
}
function processHeader(self, state, key, value, validate, lenient) {
if (validate)
validateHeaderName(key);
// If key is content-disposition and there is content-length
// encode the value in latin1
// https://www.rfc-editor.org/rfc/rfc6266#section-4.3
// Refs: https://github.com/nodejs/node/pull/46528
if (isContentDispositionField(key) && self._contentLength) {
// The value could be an array here
if (ArrayIsArray(value)) {
for (let i = 0; i < value.length; i++) {
value[i] = Buffer.from(value[i], 'latin1');
}
} else {
value = Buffer.from(value, 'latin1');
}
}
if (ArrayIsArray(value)) {
if (
(value.length < 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++)
storeHeader(self, state, key, value[i], validate, lenient);
return;
}
value = value.join('; ');
}
storeHeader(self, state, key, value, validate, lenient);
}
function storeHeader(self, state, key, value, validate, lenient) {
if (validate)
validateHeaderValue(key, value, lenient);
state.header += key + ': ' + value + '\r\n';
matchHeader(self, state, key, value);
}
function matchHeader(self, state, field, value) {
if (field.length < 4 || field.length > 17)
return;
field = field.toLowerCase();
switch (field) {
case 'connection':
state.connection = true;
self._removedConnection = false;
if (RE_CONN_CLOSE.test(value))
self._last = true;
else
self.shouldKeepAlive = true;
break;
case 'transfer-encoding':
state.te = true;
self._removedTE = false;
if (RE_TE_CHUNKED.test(value))
self.chunkedEncoding = true;
break;
case 'content-length':
state.contLen = true;
self._contentLength = +value;
self._removedContLen = false;
break;
case 'date':
case 'expect':
case 'trailer':
state[field] = true;
break;
case 'keep-alive':
self._defaultKeepAlive = false;
break;
}
}
const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFrames((name, label) => {
if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) {
throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name);
}
}));
const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => {
if (value === undefined) {
throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
}
if (checkInvalidHeaderChar(value, lenient)) {
debug('Header "%s" contains invalid characters', name);
throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name);
}
}));
function parseUniqueHeadersOption(headers) {
if (!ArrayIsArray(headers)) {
return null;
}
const unique = new SafeSet();
const l = headers.length;
for (let i = 0; i < l; i++) {
unique.add(headers[i].toLowerCase());
}
return unique;
}
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('set');
}
validateHeaderName(name);
if (value === undefined) {
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
debug('Header "%s" contains invalid characters', name);
throw new ERR_INVALID_CHAR('header content', name);
}
let headers = this[kOutHeaders];
if (headers === null)
this[kOutHeaders] = headers = { __proto__: null };
headers[name.toLowerCase()] = [name, value];
return this;
};
OutgoingMessage.prototype.setHeaders = function setHeaders(headers) {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('set');
}
if (
!headers ||
ArrayIsArray(headers) ||
typeof headers.keys !== 'function' ||
typeof headers.get !== 'function'
) {
throw new ERR_INVALID_ARG_TYPE('headers', ['Headers', 'Map'], headers);
}
// Headers object joins multiple cookies with a comma when using
// the getter to retrieve the value,
// unless iterating over the headers directly.
// We also cannot safely split by comma.
// To avoid setHeader overwriting the previous value we push
// set-cookie values in array and set them all at once.
let cookies = null;
for (const { 0: key, 1: value } of headers) {
if (key === 'set-cookie') {
if (ArrayIsArray(value)) {
cookies ??= [];
cookies.push(...value);
} else {
cookies ??= [];
cookies.push(value);
}
continue;
}
this.setHeader(key, value);
}
if (cookies != null) {
this.setHeader('set-cookie', cookies);
}
return this;
};
OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('append');
}
validateHeaderName(name);
if (value === undefined) {
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
debug('Header "%s" contains invalid characters', name);
throw new ERR_INVALID_CHAR('header content', name);
}
const field = name.toLowerCase();
const headers = this[kOutHeaders];
if (headers === null || !headers[field]) {
return this.setHeader(name, value);
}
// Prepare the field for appending, if required
if (!ArrayIsArray(headers[field][1])) {
headers[field][1] = [headers[field][1]];
}
const existingValues = headers[field][1];
if (ArrayIsArray(value)) {
for (let i = 0, length = value.length; i < length; i++) {
existingValues.push(value[i]);
}
} else {
existingValues.push(value);
}
return this;
};
OutgoingMessage.prototype.getHeader = function getHeader(name) {
validateString(name, 'name');
const headers = this[kOutHeaders];
if (headers === null)
return;
const entry = headers[name.toLowerCase()];
return entry?.[1];
};
// Returns an array of the names of the current outgoing headers.
OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() {
return this[kOutHeaders] !== null ? ObjectKeys(this[kOutHeaders]) : [];
};
// Returns an array of the names of the current outgoing raw headers.
OutgoingMessage.prototype.getRawHeaderNames = function getRawHeaderNames() {
const headersMap = this[kOutHeaders];
if (headersMap === null) return [];
const values = ObjectValues(headersMap);
const headers = Array(values.length);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0, l = values.length; i < l; i++) {
headers[i] = values[i][0];
}
return headers;
};
// Returns a shallow copy of the current outgoing headers.
OutgoingMessage.prototype.getHeaders = function getHeaders() {
const headers = this[kOutHeaders];
const ret = { __proto__: null };
if (headers) {
const keys = ObjectKeys(headers);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const val = headers[key][1];
ret[key] = val;
}
}
return ret;
};
OutgoingMessage.prototype.hasHeader = function hasHeader(name) {
validateString(name, 'name');
return this[kOutHeaders] !== null &&
!!this[kOutHeaders][name.toLowerCase()];
};
OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
validateString(name, 'name');
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('remove');
}
const key = name.toLowerCase();
switch (key) {
case 'connection':
this._removedConnection = true;
break;
case 'content-length':
this._removedContLen = true;
break;
case 'transfer-encoding':
this._removedTE = true;
break;
case 'date':
this.sendDate = false;
break;
}
if (this[kOutHeaders] !== null) {
delete this[kOutHeaders][key];
}
};
OutgoingMessage.prototype._implicitHeader = function _implicitHeader() {
throw new ERR_METHOD_NOT_IMPLEMENTED('_implicitHeader()');
};
ObjectDefineProperty(OutgoingMessage.prototype, 'headersSent', {
__proto__: null,
configurable: true,
enumerable: true,
get: function() { return !!this._header; },
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableEnded', {
__proto__: null,
get: function() { return this.finished; },
});
ObjectDefineProperty(OutgoingMessage.prototype, 'writableNeedDrain', {
__proto__: null,
get: function() {
return !this.destroyed && !this.finished && this[kNeedDrain];
},
});
const crlf_buf = Buffer.from('\r\n');
OutgoingMessage.prototype.write = function write(chunk, encoding, callback) {
if (typeof encoding === 'function') {
callback = encoding;
encoding = null;
}
const ret = write_(this, chunk, encoding, callback, false);
if (!ret)
this[kNeedDrain] = true;
return ret;
};
function onError(msg, err, callback) {
if (msg.destroyed) {
return;
}
const triggerAsyncId = msg.socket ? msg.socket[async_id_symbol] : undefined;
defaultTriggerAsyncIdScope(triggerAsyncId,
process.nextTick,
emitErrorNt,
msg,
err,
callback);
}
function emitErrorNt(msg, err, callback) {
callback(err);
if (typeof msg.emit === 'function' && !msg.destroyed) {
msg.emit('error', err);
}
}
function strictContentLength(msg) {
return (
msg.strictContentLength &&
msg._contentLength != null &&
msg._hasBody &&
!msg._removedContLen &&
!msg.chunkedEncoding &&
!msg.hasHeader('transfer-encoding')
);
}
function write_(msg, chunk, encoding, callback, fromEnd) {
if (typeof callback !== 'function')
callback = nop;
if (chunk === null) {
throw new ERR_STREAM_NULL_VALUES();
} else if (typeof chunk !== 'string' && !isUint8Array(chunk)) {
throw new ERR_INVALID_ARG_TYPE(
'chunk', ['string', 'Buffer', 'Uint8Array'], chunk);
}
let err;
if (msg.finished) {
err = new ERR_STREAM_WRITE_AFTER_END();
} else if (msg.destroyed) {
err = new ERR_STREAM_DESTROYED('write');
}
if (err) {
if (!msg.destroyed) {
onError(msg, err, callback);
} else {
process.nextTick(callback, err);
}
return false;
}
let len;
if (msg.strictContentLength) {
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
if (
strictContentLength(msg) &&
(fromEnd ? msg[kBytesWritten] + len !== msg._contentLength : msg[kBytesWritten] + len > msg._contentLength)
) {
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(len + msg[kBytesWritten], msg._contentLength);
}
msg[kBytesWritten] += len;
}
if (!msg._header) {
if (fromEnd) {
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
msg._contentLength = len;
}
msg._implicitHeader();
}
if (!msg._hasBody) {
if (msg[kRejectNonStandardBodyWrites]) {
throw new ERR_HTTP_BODY_NOT_ALLOWED();
} else {
debug('This type of response MUST NOT have a body. ' +
'Ignoring write() calls.');
process.nextTick(callback);
return true;
}
}
if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
msg.socket.cork();
process.nextTick(connectionCorkNT, msg.socket);
}
let ret;
if (msg.chunkedEncoding && chunk.length !== 0) {
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
if (msg[kCorked] && msg._headerSent) {
msg[kChunkedBuffer].push(chunk, encoding, callback);
msg[kChunkedLength] += len;
ret = msg[kChunkedLength] < msg[kHighWaterMark];
} else {
msg._send(len.toString(16), 'latin1', null);
msg._send(crlf_buf, null, null);
msg._send(chunk, encoding, null, len);
ret = msg._send(crlf_buf, null, callback);
}
} else {
ret = msg._send(chunk, encoding, callback, len);
}
debug('write ret = ' + ret);
return ret;
}
function connectionCorkNT(conn) {
conn.uncork();
}
OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
if (this.finished) {
throw new ERR_HTTP_HEADERS_SENT('set trailing');
}
this._trailer = '';
const keys = ObjectKeys(headers);
const isArray = ArrayIsArray(headers);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0, l = keys.length; i < l; i++) {
let field, value;
const key = keys[i];
if (isArray) {
field = headers[key][0];
value = headers[key][1];
} else {
field = key;
value = headers[key];
}
validateHeaderName(field, 'Trailer name');
// 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()))
) {
for (let j = 0, l = value.length; j < l; j++) {
if (checkInvalidHeaderChar(value[j], lenient)) {
debug('Trailer "%s"[%d] contains invalid characters', field, j);
throw new ERR_INVALID_CHAR('trailer content', field);
}
this._trailer += field + ': ' + value[j] + '\r\n';
}
} else {
if (isArrayValue) {
value = value.join('; ');
}
if (checkInvalidHeaderChar(value, lenient)) {
debug('Trailer "%s" contains invalid characters', field);
throw new ERR_INVALID_CHAR('trailer content', field);
}
this._trailer += field + ': ' + value + '\r\n';
}
}
};
// Deliver end() callbacks, mirroring Writable: null on successful finish,
// otherwise the error that prevented all data from being flushed.
function flushEndCallbacks(msg, err) {
const callbacks = msg[kEndCallbacks];
if (callbacks === null)
return;
msg[kEndCallbacks] = null;
for (let i = 0; i < callbacks.length; i++)
callbacks[i](err);
}
function getEndCallbackError(msg) {
return msg[kErrored] ??
msg[kSocket]?.errored ??
new ERR_STREAM_DESTROYED('end');
}
function queueEndCallback(msg, callback) {
if (msg[kWritableFinished]) {
callback(new ERR_STREAM_ALREADY_FINISHED('end'));
return;
}
if (msg[kFlushError] !== null) {
process.nextTick(callback, msg[kFlushError]);
return;
}
msg[kEndCallbacks] ??= [];
msg[kEndCallbacks].push(callback);
}
function onFinish(outmsg, err) {
if (err ||
outmsg[kErrored] ||
outmsg[kSocket]?.errored ||
outmsg[kSocket]?._hadError) {
outmsg[kFlushError] = err ?? getEndCallbackError(outmsg);
flushEndCallbacks(outmsg, outmsg[kFlushError]);
return;
}
outmsg[kWritableFinished] = true;
flushEndCallbacks(outmsg, null);
outmsg.emit('finish');
}
OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
if (typeof chunk === 'function') {
callback = chunk;
chunk = null;
encoding = null;
} else if (typeof encoding === 'function') {
callback = encoding;
encoding = null;
}
if (chunk) {
if (this.finished) {
onError(this,
new ERR_STREAM_WRITE_AFTER_END(),
typeof callback !== 'function' ? nop : callback);
return this;
}
if (this[kSocket]) {
this[kSocket].cork();
}
write_(this, chunk, encoding, null, true);
} else if (this.finished) {
if (typeof callback === 'function') {
queueEndCallback(this, callback);
}
return this;
} else if (!this._header) {
if (this[kSocket]) {
this[kSocket].cork();
}
this._contentLength = 0;
this._implicitHeader();
}
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);
}
const finish = onFinish.bind(undefined, this);
if (this._hasBody && this.chunkedEncoding) {
this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish);
} else if (!this._headerSent || this.writableLength || chunk) {
this._send('', 'latin1', finish);
} else {
process.nextTick(finish);
}
if (this[kSocket]) {
// Fully uncork connection on end().
this[kSocket]._writableState.corked = 1;
this[kSocket].uncork();
}
this[kCorked] = 1;
this.uncork();
this.finished = true;
// There is the first message on the outgoing queue, and we've sent
// everything to the socket.
debug('outgoing message end.');
if (this.outputData.length === 0 &&
this[kSocket] &&
this[kSocket]._httpMessage === this) {
this._finish();
}
return this;
};
// 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.
OutgoingMessage.prototype._finish = function _finish() {
assert(this[kSocket]);
this.emit('prefinish');
};
// This logic is probably a bit confusing. Let me explain a bit:
//
// In both HTTP servers and clients it is possible to queue up several
// outgoing messages. This is easiest to imagine in the case of a client.
// Take the following situation:
//
// req1 = client.request('GET', '/');
// req2 = client.request('POST', '/');
//
// When the user does
//
// req2.write('hello world\n');
//
// it's possible that the first request has not been completely flushed to
// the socket yet. Thus the outgoing messages need to be prepared to queue
// up data internally before sending it on further to the socket's queue.
//
// This function, _flush(), is called by both the Server and Client
// to attempt to flush any pending messages out to the socket.
OutgoingMessage.prototype._flush = function _flush() {
const socket = this[kSocket];
if (socket?.writable) {
// There might be remaining data in this.output; write it out
const ret = this._flushOutput(socket);
if (this.finished) {
// This is a queue to the server or client to bring in the next this.
this._finish();
} else if (this[kNeedDrain] && ret !== false) {
// Socket accepted all data without backpressure - it won't emit
// drain, so we emit it since the OM buffer is now clear.
this[kNeedDrain] = false;
this.emit('drain');
}
}
};
OutgoingMessage.prototype._flushOutput = function _flushOutput(socket) {
const outputLength = this.outputData.length;
if (outputLength <= 0)
return undefined;
const outputData = this.outputData;
socket.cork();
let ret;
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0; i < outputLength; i++) {
const { data, encoding, callback } = outputData[i];
// Avoid any potential ref to Buffer in new generation from old generation
outputData[i].data = null;
ret = socket.write(data, encoding, callback);
}
socket.uncork();
this.outputData = [];
this._onPendingData(-this.outputSize);
this.outputSize = 0;
return ret;
};
OutgoingMessage.prototype.flushHeaders = function flushHeaders() {
if (!this._header) {
this._implicitHeader();
}
// Force-flush the headers.
this._send('');
};
OutgoingMessage.prototype.pipe = function pipe() {
// OutgoingMessage should be write-only. Piping from it is disabled.
this.emit('error', new ERR_STREAM_CANNOT_PIPE());
};
OutgoingMessage.prototype[EE.captureRejectionSymbol] =
function(err, event) {
this.destroy(err);
};
module.exports = {
kHighWaterMark,
kUniqueHeaders,
parseUniqueHeadersOption,
validateHeaderName,
validateHeaderValue,
OutgoingMessage,
};