Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/_http_incoming.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -529,6 +529,7 @@ function onError(self, error, cb) {
module.exports = {
IncomingMessage,
kDetachAbortSignal,
kHeadersCount,
readStart,
readStop,
};
50 changes: 35 additions & 15 deletions lib/_http_outgoing.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ 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 { kOutHeaders, utcDateHeader, kNeedDrain } = require('internal/http');
const { Buffer } = require('buffer');
const {
_checkIsHttpToken: checkIsHttpToken,
Expand DownExpand Up@@ -89,6 +89,11 @@ const kChunkedLength = Symbol('kChunkedLength');
const kUniqueHeaders = Symbol('kUniqueHeaders');
const kBytesWritten = Symbol('kBytesWritten');
const kErrored = Symbol('errored');
const kLenientCache = Symbol('kLenientCache');

let keepAliveTimeoutCache = -1;
let keepAliveMaxCache = -1;
let keepAliveHeaderCache = '';
const kWritableFinished = Symbol('kWritableFinished');
const kEndCallbacks = Symbol('kEndCallbacks');
const kFlushError = Symbol('kFlushError');
Expand DownExpand Up@@ -169,6 +174,7 @@ function OutgoingMessage(options) {
this[kFlushError] = null;
this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
this[kLenientCache] = null;
}
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
ObjectSetPrototypeOf(OutgoingMessage, Stream);
Expand All@@ -178,27 +184,34 @@ ObjectSetPrototypeOf(OutgoingMessage, Stream);
// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
// Falls back to global --insecure-http-parser flag.
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
// The underlying options cannot change during the lifetime of a message:
// compute the lookup chain only once per message.
this[kLenientCache] ??= isLenientHeaderValidation(this);
return this[kLenientCache];
};

function isLenientHeaderValidation(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,
Expand DownExpand Up@@ -508,7 +521,7 @@ function _storeHeader(firstLine, headers) {

// Date header
if (this.sendDate && !state.date) {
header += 'Date: ' + utcDate() + '\r\n';
header += utcDateHeader();
}

// Force the connection to close when the response is a 204 No Content or
Expand DownExpand Up@@ -541,14 +554,21 @@ function _storeHeader(firstLine, headers) {
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}`;
// The keep-alive header lines are identical for every response of a
// given server: cache the last rendered value.
const timeout = this._keepAliveTimeout;
const max = ~~this._maxRequestsPerSocket;
if (timeout !== keepAliveTimeoutCache || max !== keepAliveMaxCache) {
keepAliveTimeoutCache = timeout;
keepAliveMaxCache = max;
keepAliveHeaderCache = 'Connection: keep-alive\r\n' +
`Keep-Alive: timeout=${MathFloor(timeout / 1000)}` +
(max > 0 ? `, max=${max}` : '') + '\r\n';
}
header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`;
header += keepAliveHeaderCache;
} else {
header += 'Connection: keep-alive\r\n';
}
} else {
this._last = true;
Expand Down
100 changes: 82 additions & 18 deletions lib/_http_server.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,6 +73,7 @@ const {
const {
IncomingMessage,
kDetachAbortSignal,
kHeadersCount,
} = require('_http_incoming');
const {
ConnResetException,
Expand DownExpand Up@@ -117,6 +118,10 @@ const kServerResponseStatistics = Symbol('ServerResponseStatistics');
const kUpgradeStream = Symbol('UpgradeStream');

const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
const kConnectionState = Symbol('ConnectionState');
const kResponseOptions = Symbol('ResponseOptions');

const statusLineCache = [];

const {
hasObserver,
Expand DownExpand Up@@ -475,10 +480,18 @@ function writeHead(statusCode, reason, obj) {
headers = obj;
}

if (checkInvalidHeaderChar(this.statusMessage))
throw new ERR_INVALID_CHAR('statusMessage');
let statusLine;
if (this.statusMessage === STATUS_CODES[statusCode]) {
// Default reason phrases contain no invalid characters and are shared
// across responses: cache the whole status line.
statusLine = statusLineCache[statusCode] ??=
`HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
} else {
if (checkInvalidHeaderChar(this.statusMessage))
throw new ERR_INVALID_CHAR('statusMessage');

const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
}

if (statusCode === 204 || statusCode === 304 ||
(statusCode >= 100 && statusCode <= 199)) {
Expand DownExpand Up@@ -827,7 +840,11 @@ function connectionListenerInternal(server, socket) {
outgoingData: 0,
requestsCount: 0,
keepAliveTimeoutSet: false,
onPendingData: null,
server,
socket,
};
state.onPendingData = updateOutgoingData.bind(undefined, socket, state);
state.onData = socketOnData.bind(undefined,
server, socket, parser, state);
state.onEnd = socketOnEnd.bind(undefined,
Expand DownExpand Up@@ -1264,8 +1281,61 @@ function emitCloseNT(self) {
}
}

function hasBodyHeaders(headers) {
return ('content-length' in headers) || ('transfer-encoding' in headers);
// Check for the presence of a request header by scanning rawHeaders instead
// of materializing the req.headers object. Only the first req[kHeadersCount]
// entries are considered, matching what the req.headers getter exposes.
function hasRequestHeader(req, length, lowerName) {
const rawHeaders = req.rawHeaders;
const count = req[kHeadersCount];
for (let i = 0; i < count; i += 2) {
const key = rawHeaders[i];
if (key.length === length &&
(key === lowerName || key.toLowerCase() === lowerName)) {
return true;
}
}
return false;
}

function hasBodyHeaders(req) {
const rawHeaders = req.rawHeaders;
const count = req[kHeadersCount];
for (let i = 0; i < count; i += 2) {
const length = rawHeaders[i].length;
if (length === 14 || length === 17) {
const key = rawHeaders[i].toLowerCase();
if (key === 'content-length' || key === 'transfer-encoding') {
return true;
}
}
}
return false;
}

// The options object passed to the ServerResponse constructor is identical
// for every request of a given server: cache it. Custom response classes get
// a fresh object since they may retain or mutate it.
function getResponseOptions(server, socket) {
const highWaterMark = socket.writableHighWaterMark;
const rejectNonStandardBodyWrites = server.rejectNonStandardBodyWrites;
if (server[kServerResponse] !== ServerResponse) {
return { highWaterMark, rejectNonStandardBodyWrites };
}
let options = server[kResponseOptions];
if (options === undefined ||
options.highWaterMark !== highWaterMark ||
options.rejectNonStandardBodyWrites !== rejectNonStandardBodyWrites) {
options = server[kResponseOptions] =
{ highWaterMark, rejectNonStandardBodyWrites };
}
return options;
}

// Shared 'finish' listener: everything resOnFinish needs is reachable from
// the response, avoiding a bound function per request.
function onResponseFinish() {
const state = this[kConnectionState];
resOnFinish(this.req, this, state.socket, state, state.server);
}

// The following callback is issued after the headers have been read on a
Expand DownExpand Up@@ -1298,15 +1368,11 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
}
}

const res = new server[kServerResponse](req,
{
highWaterMark: socket.writableHighWaterMark,
rejectNonStandardBodyWrites: server.rejectNonStandardBodyWrites,
});
const res = new server[kServerResponse](req, getResponseOptions(server, socket));
res._keepAliveTimeout = server.keepAliveTimeout;
res._maxRequestsPerSocket = server.maxRequestsPerSocket;
res._onPendingData = updateOutgoingData.bind(undefined,
socket, state);
res._onPendingData = state.onPendingData;
res[kConnectionState] = state;

res.shouldKeepAlive = keepAlive;
res[kUniqueHeaders] = server[kUniqueHeaders];
Expand All@@ -1321,7 +1387,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
}

// Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req.headers);
const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req);

if (shouldOptimize) {
// Fast processing where emitting 'data', 'end' and 'close' events is
Expand All@@ -1342,9 +1408,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {

// When we're finished writing the response, check if this is the last
// response, if so destroy the socket.
res.on('finish',
resOnFinish.bind(undefined,
req, res, socket, state, server));
res.on('finish', onResponseFinish);

let handled = false;

Expand All@@ -1354,7 +1418,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
// From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
// A server MUST respond with a 400 (Bad Request) status code to any
// HTTP/1.1 request message that lacks a Host header field
if (server.requireHostHeader && req.headers.host === undefined) {
if (server.requireHostHeader && !hasRequestHeader(req, 4, 'host')) {
res.writeHead(400, ['Connection', 'close']);
res.end();
return 0;
Expand All@@ -1377,7 +1441,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
server.emit('dropRequest', req, socket);
res.writeHead(503);
res.end();
} else if (req.headers.expect !== undefined) {
} else if (hasRequestHeader(req, 6, 'expect')) {
handled = true;

if (continueExpression.test(req.headers.expect)) {
Expand Down
10 changes: 10 additions & 0 deletions lib/internal/http.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,12 +22,20 @@ const { Buffer } = require('buffer');
const { isIPv4 } = require('internal/net');
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
let utcCache;
let utcHeaderCache;

function utcDate() {
if (!utcCache) cache();
return utcCache;
}

// The complete `Date: ...\r\n` header line, cached alongside utcDate() so
// that the serializer does not re-concatenate it for every response.
function utcDateHeader() {
utcHeaderCache ||= 'Date: ' + utcDate() + '\r\n';
return utcHeaderCache;
}

function cache() {
const d = new Date();
utcCache = d.toUTCString();
Expand All@@ -36,6 +44,7 @@ function cache() {

function resetCache() {
utcCache = undefined;
utcHeaderCache = undefined;
}

let traceEventId = 0;
Expand DownExpand Up@@ -277,6 +286,7 @@ module.exports = {
checkShouldUseProxy,
parseProxyConfigFromEnv,
utcDate,
utcDateHeader,
traceBegin,
traceEnd,
getNextTraceEventId,
Expand Down
Loading