Previously, socketOnDrain could be invoked synchronously from _flushOutput (via _onPendingData -> updateOutgoingData) while the bytes just handed to the socket were still buffered and while outputSize had not yet been reset on the OutgoingMessage. The 'drain' event fired even though res.writableLength was non-zero, breaking the invariant a user would reasonably expect after `while (!res.write(...));`. Gate the emission in socketOnDrain on msg.writableLength === 0 (which also covers outputSize + chunked buffer + socket.writableLength), and apply the same check in OutgoingMessage._flush so that 'drain' is only emitted when the response is genuinely drained. The socket's own 'drain' event will otherwise propagate through socketOnDrain when the socket buffer actually empties. Signed-off-by: Robert Nagy <ronagy@icloud.com> Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> PR-URL: https://github.com/nodejs/node/pull/62936 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1211 lines
34 KiB
JavaScript
1211 lines
34 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,
|
|
} = 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 } = 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 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) {
|
|
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[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
|
|
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
|
|
}
|
|
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
|
|
ObjectSetPrototypeOf(OutgoingMessage, Stream);
|
|
|
|
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.finished &&
|
|
this.outputSize === 0 &&
|
|
(!this[kSocket] || this[kSocket].writableLength === 0)
|
|
);
|
|
},
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
if (headers) {
|
|
if (headers === this[kOutHeaders]) {
|
|
for (const key in headers) {
|
|
const entry = headers[key];
|
|
processHeader(this, state, entry[0], entry[1], false);
|
|
}
|
|
} 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);
|
|
}
|
|
} 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);
|
|
}
|
|
}
|
|
} else {
|
|
for (const key in headers) {
|
|
if (ObjectHasOwn(headers, key)) {
|
|
processHeader(this, state, key, headers[key], true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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);
|
|
return;
|
|
}
|
|
value = value.join('; ');
|
|
}
|
|
storeHeader(self, state, key, value, validate);
|
|
}
|
|
|
|
function storeHeader(self, state, key, value, validate) {
|
|
if (validate)
|
|
validateHeaderValue(key, value);
|
|
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) => {
|
|
if (value === undefined) {
|
|
throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
|
|
}
|
|
if (checkInvalidHeaderChar(value)) {
|
|
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);
|
|
validateHeaderValue(name, value);
|
|
|
|
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);
|
|
validateHeaderValue(name, value);
|
|
|
|
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) {
|
|
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);
|
|
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])) {
|
|
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)) {
|
|
debug('Trailer "%s" contains invalid characters', field);
|
|
throw new ERR_INVALID_CHAR('trailer content', field);
|
|
}
|
|
this._trailer += field + ': ' + value + '\r\n';
|
|
}
|
|
}
|
|
};
|
|
|
|
function onFinish(outmsg) {
|
|
if (outmsg?.socket?._hadError) return;
|
|
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') {
|
|
if (!this.writableFinished) {
|
|
this.on('finish', callback);
|
|
} else {
|
|
callback(new ERR_STREAM_ALREADY_FINISHED('end'));
|
|
}
|
|
}
|
|
return this;
|
|
} else if (!this._header) {
|
|
if (this[kSocket]) {
|
|
this[kSocket].cork();
|
|
}
|
|
|
|
this._contentLength = 0;
|
|
this._implicitHeader();
|
|
}
|
|
|
|
if (typeof callback === 'function')
|
|
this.once('finish', 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
|
|
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] && this.writableLength === 0) {
|
|
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,
|
|
};
|