Commit 12ce81920a1 for nodejs
commit 12ce81920a1f526e681153c0cb4b1456867c328d
Author: Sebastian Beltran <bjohansebas@gmail.com>
Date: Wed Oct 14 19:52:34 2026 -0500
lib: deprecate _http_*
Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/58535
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2e8dac324f7..2a27e611350 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -73,6 +73,7 @@
/lib/https.js @nodejs/crypto @nodejs/net @nodejs/http
/src/node_http_common* @nodejs/http @nodejs/http2 @nodejs/net
/src/node_http_parser.cc @nodejs/http @nodejs/net
+/lib/internal/http/* @nodejs/http @nodejs/net
# http2
diff --git a/benchmark/http/check_invalid_header_char.js b/benchmark/http/check_invalid_header_char.js
index bd250cd5fe0..5d1ffe74e58 100644
--- a/benchmark/http/check_invalid_header_char.js
+++ b/benchmark/http/check_invalid_header_char.js
@@ -1,7 +1,6 @@
'use strict';
const common = require('../common.js');
-const _checkInvalidHeaderChar = require('_http_common')._checkInvalidHeaderChar;
const groupedInputs = {
// Representative set of inputs from an AcmeAir benchmark run:
@@ -51,9 +50,13 @@ const inputs = [
const bench = common.createBenchmark(main, {
input: inputs.concat(Object.keys(groupedInputs)),
n: [1e6],
+}, {
+ flags: ['--expose-internals', '--no-warnings'],
});
function main({ n, input }) {
+ const checkInvalidHeaderChar = require('internal/http/common')._checkInvalidHeaderChar;
+
let inputs = [input];
if (Object.hasOwn(groupedInputs, input)) {
inputs = groupedInputs[input];
@@ -62,7 +65,7 @@ function main({ n, input }) {
const len = inputs.length;
bench.start();
for (let i = 0; i < n; i++) {
- _checkInvalidHeaderChar(inputs[i % len]);
+ checkInvalidHeaderChar(inputs[i % len]);
}
bench.end(n);
}
diff --git a/benchmark/http/check_is_http_token.js b/benchmark/http/check_is_http_token.js
index 2137a73e547..553608571aa 100644
--- a/benchmark/http/check_is_http_token.js
+++ b/benchmark/http/check_is_http_token.js
@@ -1,7 +1,6 @@
'use strict';
const common = require('../common.js');
-const _checkIsHttpToken = require('_http_common')._checkIsHttpToken;
const bench = common.createBenchmark(main, {
key: [
@@ -38,12 +37,16 @@ const bench = common.createBenchmark(main, {
'alternate-protocol:', // slow bailout
],
n: [1e6],
+}, {
+ flags: ['--expose-internals', '--no-warnings'],
});
function main({ n, key }) {
+ const checkIsHttpToken = require('internal/http/common')._checkIsHttpToken;
+
bench.start();
for (let i = 0; i < n; i++) {
- _checkIsHttpToken(key);
+ checkIsHttpToken(key);
}
bench.end(n);
}
diff --git a/benchmark/http/set_header.js b/benchmark/http/set_header.js
index f2236696f1c..0938c1e9cc2 100644
--- a/benchmark/http/set_header.js
+++ b/benchmark/http/set_header.js
@@ -1,7 +1,7 @@
'use strict';
const common = require('../common.js');
-const { OutgoingMessage } = require('_http_outgoing');
+const { OutgoingMessage } = require('http');
const bench = common.createBenchmark(main, {
value: [
diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md
index 932e0ca1c1f..09a4380cfed 100644
--- a/doc/api/deprecations.md
+++ b/doc/api/deprecations.md
@@ -4443,6 +4443,9 @@ Creating SHAKE-128 and SHAKE-256 digests without an explicit
<!-- YAML
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/58535
+ description: Runtime deprecation.
- version:
- v24.6.0
- v22.19.0
diff --git a/lib/_http_agent.js b/lib/_http_agent.js
index 3871c5b56a2..d719f4d70b6 100644
--- a/lib/_http_agent.js
+++ b/lib/_http_agent.js
@@ -21,695 +21,12 @@
'use strict';
-const {
- NumberIsFinite,
- NumberParseInt,
- ObjectKeys,
- ObjectSetPrototypeOf,
- ObjectValues,
- Symbol,
-} = primordials;
-
-const net = require('net');
-const EventEmitter = require('events');
-let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
- debug = fn;
-});
-const {
- parseProxyConfigFromEnv,
- kProxyConfig,
- checkShouldUseProxy,
- kWaitForProxyTunnel,
- getGlobalAgent,
-} = require('internal/http');
-const { AsyncResource } = require('async_hooks');
-const {
- async_id_symbol,
- owner_symbol,
-} = require('internal/async_hooks').symbols;
-const {
- deprecateInstantiation,
- getLazy,
- kEmptyObject,
- once,
-} = require('internal/util');
-const {
- onStreamRead,
-} = require('internal/stream_base_commons');
-const {
- kReadBytesOrError,
- streamBaseState,
-} = internalBinding('stream_wrap');
-const {
- validateNumber,
- validateOneOf,
- validateString,
-} = require('internal/validators');
-const assert = require('internal/assert');
-const { getOptionValue } = require('internal/options');
-
-const kOnKeylog = Symbol('onkeylog');
-const kRequestOptions = Symbol('requestOptions');
-const kRequestAsyncResource = Symbol('requestAsyncResource');
-const kFreeSocketDataGuard = Symbol('freeSocketDataGuard');
-
-// New Agent code.
-
-// The largest departure from the previous implementation is that
-// an Agent instance holds connections for a variable number of host:ports.
-// Surprisingly, this is still API compatible as far as third parties are
-// concerned. The only code that really notices the difference is the
-// request object.
-
-// Another departure is that all code related to HTTP parsing is in
-// ClientRequest.onSocket(). The Agent is now *strictly*
-// concerned with managing a connection pool.
-
-class ReusedHandle {
- constructor(type, handle) {
- this.type = type;
- this.handle = handle;
- }
-}
-
-function freeSocketErrorListener(err) {
- const socket = this;
- debug('SOCKET ERROR on FREE socket:', err.message, err.stack);
- socket.destroy();
- socket.emit('agentRemove');
-}
-
-// Guard against unsolicited data arriving while a socket is idle in the
-// freeSockets pool. When the HTTPParser is detached the data would sit
-// in the TCP buffer and be silently consumed as the response for the
-// *next* request that reuses the socket (response-queue poisoning).
-// See: https://hackerone.com/reports/3582376
-function freeSocketOnReadGuard() {
- const nread = streamBaseState[kReadBytesOrError];
- if (nread === 0) return;
-
- debug('READ on FREE socket - destroying poisoned socket');
- this[owner_symbol].destroy();
-}
-
-function installFreeSocketDataGuard(socket) {
- if (socket.readableLength > 0) {
- debug('BUFFERED DATA on FREE socket - destroying poisoned socket');
- socket.destroy();
- return;
- }
-
- if (socket.connecting) {
- socket[kFreeSocketDataGuard] = function onConnect() {
- socket[kFreeSocketDataGuard] = null;
- installFreeSocketDataGuard(socket);
- };
- socket.once('connect', socket[kFreeSocketDataGuard]);
- return;
- }
-
- const handle = socket._handle;
- if (handle) {
- handle.onread = freeSocketOnReadGuard;
- if (!handle.reading) {
- handle.reading = true;
- const err = handle.readStart();
- if (err) socket.destroy();
- }
- }
-}
-
-function removeFreeSocketDataGuard(socket) {
- if (socket[kFreeSocketDataGuard]) {
- socket.removeListener('connect', socket[kFreeSocketDataGuard]);
- socket[kFreeSocketDataGuard] = null;
- }
-
- const handle = socket._handle;
- if (handle?.onread === freeSocketOnReadGuard) {
- handle.onread = onStreamRead;
- }
-}
-
-function Agent(options) {
- if (!(this instanceof Agent)) {
- return deprecateInstantiation(Agent, 'DEP0195', options);
- }
-
- EventEmitter.call(this);
-
- this.options = { __proto__: null, ...options };
-
- this.defaultPort = this.options.defaultPort || 80;
- this.protocol = this.options.protocol || 'http:';
-
- if (this.options.noDelay === undefined)
- this.options.noDelay = true;
-
- // Don't confuse net and make it think that we're connecting to a pipe
- this.options.path = null;
- this.requests = { __proto__: null };
- this.sockets = { __proto__: null };
- this.freeSockets = { __proto__: null };
- this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;
- this.keepAlive = this.options.keepAlive || false;
- this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
- this.maxFreeSockets = this.options.maxFreeSockets || 256;
- this.scheduling = this.options.scheduling || 'lifo';
- this.maxTotalSockets = this.options.maxTotalSockets;
- this.totalSocketCount = 0;
-
- this.agentKeepAliveTimeoutBuffer =
- typeof this.options.agentKeepAliveTimeoutBuffer === 'number' &&
- this.options.agentKeepAliveTimeoutBuffer >= 0 &&
- NumberIsFinite(this.options.agentKeepAliveTimeoutBuffer) ?
- this.options.agentKeepAliveTimeoutBuffer :
- 1000;
-
- const proxyEnv = this.options.proxyEnv;
- if (typeof proxyEnv === 'object' && proxyEnv !== null) {
- this[kProxyConfig] = parseProxyConfigFromEnv(proxyEnv, this.protocol, this.keepAlive);
- debug(`new ${this.protocol} agent with proxy config`, this[kProxyConfig]);
- }
-
- validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']);
-
- if (this.maxTotalSockets !== undefined) {
- validateNumber(this.maxTotalSockets, 'maxTotalSockets', 1);
- } else {
- this.maxTotalSockets = Infinity;
- }
-
- this.on('free', (socket, options) => {
- const name = this.getName(options);
- debug('agent.on(free)', name);
-
- // TODO(ronag): socket.destroy(err) might have been called
- // before coming here and have an 'error' scheduled. In the
- // case of socket.destroy() below this 'error' has no handler
- // and could cause unhandled exception.
-
- if (!socket.writable) {
- socket.destroy();
- return;
- }
-
- const requests = this.requests[name];
- if (requests?.length) {
- const req = requests.shift();
- const reqAsyncRes = req[kRequestAsyncResource];
- if (reqAsyncRes) {
- // Run request within the original async context.
- reqAsyncRes.runInAsyncScope(() => {
- asyncResetHandle(socket);
- setRequestSocket(this, req, socket);
- });
- req[kRequestAsyncResource] = null;
- } else {
- setRequestSocket(this, req, socket);
- }
- if (requests.length === 0) {
- delete this.requests[name];
- }
- return;
- }
-
- // If there are no pending requests, then put it in
- // the freeSockets pool, but only if we're allowed to do so.
- const req = socket._httpMessage;
- if (!req || !req.shouldKeepAlive || !this.keepAlive) {
- socket.destroy();
- return;
- }
-
- const freeSockets = this.freeSockets[name] || [];
- const freeLen = freeSockets.length;
- let count = freeLen;
- if (this.sockets[name])
- count += this.sockets[name].length;
-
- if (this.totalSocketCount > this.maxTotalSockets ||
- count > this.maxSockets ||
- freeLen >= this.maxFreeSockets ||
- !this.keepSocketAlive(socket)) {
- socket.destroy();
- return;
- }
-
- this.freeSockets[name] = freeSockets;
- socket[async_id_symbol] = -1;
- socket._httpMessage = null;
- this.removeSocket(socket, options);
-
- socket.once('error', freeSocketErrorListener);
- installFreeSocketDataGuard(socket);
- freeSockets.push(socket);
- });
-
- // Don't emit keylog events unless there is a listener for them.
- this.on('newListener', maybeEnableKeylog);
-}
-ObjectSetPrototypeOf(Agent.prototype, EventEmitter.prototype);
-ObjectSetPrototypeOf(Agent, EventEmitter);
-
-function maybeEnableKeylog(eventName) {
- if (eventName === 'keylog') {
- this.removeListener('newListener', maybeEnableKeylog);
- // Future sockets will listen on keylog at creation.
- const agent = this;
- this[kOnKeylog] = function onkeylog(keylog) {
- agent.emit('keylog', keylog, this);
- };
- // Existing sockets will start listening on keylog now. Both maps hold
- // arrays of sockets keyed by name, so each bucket has to be walked.
- const sets = [this.freeSockets, this.sockets];
- for (let s = 0; s < sets.length; s++) {
- const buckets = ObjectValues(sets[s]);
- for (let b = 0; b < buckets.length; b++) {
- const sockets = buckets[b];
- for (let n = 0; n < sockets.length; n++) {
- sockets[n].on('keylog', this[kOnKeylog]);
- }
- }
- }
- }
-}
-
-const lazyTLS = getLazy(() => require('tls'));
-
-Agent.defaultMaxSockets = Infinity;
-
-// See ProxyConfig in internal/http.js for how the connection should be handled
-// when the agent is configured to use a proxy server.
-Agent.prototype.createConnection = function createConnection(...args) {
- const normalized = net._normalizeArgs(args);
- const options = normalized[0];
- const cb = normalized[1];
-
- // Check if this specific request should bypass the proxy
- const shouldUseProxy = checkShouldUseProxy(this[kProxyConfig], options);
- debug(`http createConnection should use proxy for ${options.host}:${options.port}:`, shouldUseProxy);
- if (!shouldUseProxy) { // Forward to net.createConnection if no proxying is needed.
- return net.createConnection(...args);
- }
-
- // Create a copy of the shared proxy connection options and connect
- // to the proxy server instead of the endpoint. For Agent.prototype.createConnection
- // which is used by the http agent, this is enough
- const connectOptions = {
- ...this[kProxyConfig].proxyConnectionOptions,
- };
- const proxyProtocol = this[kProxyConfig].protocol;
- if (proxyProtocol === 'http:') {
- return net.connect(connectOptions, cb);
- } else if (proxyProtocol === 'https:') {
- return lazyTLS().connect(connectOptions, cb);
- }
- // This should be unreachable because proxy config should be null for other protocols.
- assert.fail(`Unexpected proxy protocol ${proxyProtocol}`);
-
-};
-
-// Get the key for a given set of request options
-Agent.prototype.getName = function getName(options = kEmptyObject) {
- let name = options.host || 'localhost';
-
- name += ':';
- if (options.port)
- name += options.port;
-
- name += ':';
- if (options.localAddress)
- name += options.localAddress;
-
- // Pacify parallel/test-http-agent-getname by only appending
- // the ':' when options.family is set.
- if (options.family === 4 || options.family === 6)
- name += `:${options.family}`;
-
- if (options.socketPath)
- name += `:${options.socketPath}`;
-
- return name;
-};
-
-function handleSocketAfterProxy(err, req) {
- if (err.code === 'ERR_PROXY_TUNNEL') {
- if (err.proxyTunnelTimeout) {
- req.emit('timeout'); // Propagate the timeout from the tunnel to the request.
- }
- req.emit('error', err);
- }
-}
-
-Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
- localAddress/* legacy */) {
- // Legacy API: addRequest(req, host, port, localAddress)
- if (typeof options === 'string') {
- options = {
- __proto__: null,
- host: options,
- port,
- localAddress,
- };
- }
-
- // Here the agent options will override per-request options.
- options = { __proto__: null, ...options, ...this.options };
- if (options.socketPath)
- options.path = options.socketPath;
-
- normalizeServerName(options, req);
-
- const name = this.getName(options);
- this.sockets[name] ||= [];
-
- const freeSockets = this.freeSockets[name];
- let socket;
- if (freeSockets) {
- while (freeSockets.length && freeSockets[0].destroyed) {
- freeSockets.shift();
- }
- socket = this.scheduling === 'fifo' ?
- freeSockets.shift() :
- freeSockets.pop();
- if (!freeSockets.length)
- delete this.freeSockets[name];
- }
-
- const freeLen = freeSockets ? freeSockets.length : 0;
- const sockLen = freeLen + this.sockets[name].length;
-
- // Reusing a socket from the pool.
- // If the caller specified a highWaterMark that differs from the pooled
- // socket's writableHighWaterMark, sync the socket's HWM so that
- // backpressure semantics match what the caller requested.
- if (socket && options.highWaterMark != null &&
- socket.writableHighWaterMark !== options.highWaterMark) {
- debug('sync reused socket HWM (socket=%d, request=%d)',
- socket.writableHighWaterMark, options.highWaterMark);
- socket._writableState.highWaterMark = options.highWaterMark;
- }
-
- if (socket) {
- asyncResetHandle(socket);
- this.reuseSocket(socket, req);
- setRequestSocket(this, req, socket);
- this.sockets[name].push(socket);
- } else if (sockLen < this.maxSockets &&
- this.totalSocketCount < this.maxTotalSockets) {
- // If we are under maxSockets create a new one.
- this.createSocket(req, options, (err, socket) => {
- if (err) {
- handleSocketAfterProxy(err, req);
- debug('call onSocket', sockLen, freeLen);
- req.onSocket(socket, err);
- return;
- }
-
- setRequestSocket(this, req, socket);
- });
- } else {
- debug('wait for socket');
- // We are over limit so we'll add it to the queue.
- this.requests[name] ||= [];
-
- // Used to create sockets for pending requests from different origin
- req[kRequestOptions] = options;
- // Used to capture the original async context.
- req[kRequestAsyncResource] = new AsyncResource('QueuedRequest');
-
- this.requests[name].push(req);
- }
-};
-
-Agent.prototype.createSocket = function createSocket(req, options, cb) {
- // Here the agent options will override per-request options.
- options = { __proto__: null, ...options, ...this.options };
- if (options.socketPath)
- options.path = options.socketPath;
-
- normalizeServerName(options, req);
-
- // Make sure per-request timeout is respected.
- const timeout = req.timeout || this.options.timeout || undefined;
- if (timeout) {
- options.timeout = timeout;
- }
-
- const name = this.getName(options);
- options._agentKey = name;
-
- debug('createConnection', name);
- options.encoding = null;
-
- const oncreate = once((err, s) => {
- if (err)
- return cb(err);
- this.sockets[name] ||= [];
- this.sockets[name].push(s);
- this.totalSocketCount++;
- debug('sockets', name, this.sockets[name].length, this.totalSocketCount);
- installListeners(this, s, options);
- cb(null, s);
- });
- // When keepAlive is true, pass the related options to createConnection
- if (this.keepAlive) {
- options.keepAlive = this.keepAlive;
- options.keepAliveInitialDelay = this.keepAliveMsecs;
- }
-
- const newSocket = this.createConnection(options, oncreate);
- // In the case where we are proxying through a tunnel for HTTPS, only add
- // the socket to the pool and install/invoke the listeners after
- // the tunnel is successfully established, so that actual operations
- // on the socket all go through the tunnel. Errors emitted during
- // tunnel establishment will be handled in the createConnection method
- // in lib/https.js.
- if (newSocket && !newSocket[kWaitForProxyTunnel])
- oncreate(null, newSocket);
-};
-
-function normalizeServerName(options, req) {
- if (!options.servername && options.servername !== '')
- options.servername = calculateServerName(options, req);
-}
-
-function calculateServerName(options, req) {
- let servername = options.host;
- const hostHeader = req.getHeader('host');
- if (hostHeader) {
- validateString(hostHeader, 'options.headers.host');
-
- // abc => abc
- // abc:123 => abc
- // [::1] => ::1
- // [::1]:123 => ::1
- if (hostHeader[0] === '[') {
- const index = hostHeader.indexOf(']');
- if (index === -1) {
- // Leading '[', but no ']'. Need to do something...
- servername = hostHeader;
- } else {
- servername = hostHeader.substring(1, index);
- }
- } else {
- servername = hostHeader.split(':', 1)[0];
- }
- }
- // Don't implicitly set invalid (IP) servernames.
- if (net.isIP(servername))
- servername = '';
- return servername;
-}
-
-function installListeners(agent, s, options) {
- function onFree() {
- debug('CLIENT socket onFree');
- agent.emit('free', s, options);
- }
- s.on('free', onFree);
-
- function onClose(err) {
- debug('CLIENT socket onClose');
- // This is the only place where sockets get removed from the Agent.
- // If you want to remove a socket from the pool, just close it.
- // All socket errors end in a close event anyway.
- agent.totalSocketCount--;
- agent.removeSocket(s, options);
- }
- s.on('close', onClose);
-
- function onTimeout() {
- debug('CLIENT socket onTimeout');
-
- // Destroy if in free list.
- // TODO(ronag): Always destroy, even if not in free list.
- const sockets = agent.freeSockets;
- if (ObjectKeys(sockets).some((name) => sockets[name].includes(s))) {
- return s.destroy();
- }
- }
- s.on('timeout', onTimeout);
-
- function onRemove() {
- // We need this function for cases like HTTP 'upgrade'
- // (defined by WebSockets) where we need to remove a socket from the
- // pool because it'll be locked up indefinitely
- debug('CLIENT socket onRemove');
- agent.totalSocketCount--;
- agent.removeSocket(s, options);
- s.removeListener('close', onClose);
- s.removeListener('free', onFree);
- s.removeListener('timeout', onTimeout);
- s.removeListener('agentRemove', onRemove);
- }
- s.on('agentRemove', onRemove);
-
- if (agent[kOnKeylog]) {
- s.on('keylog', agent[kOnKeylog]);
- }
-}
-
-Agent.prototype.removeSocket = function removeSocket(s, options) {
- const name = this.getName(options);
- debug('removeSocket', name, 'writable:', s.writable);
- const sets = [this.sockets];
-
- // If the socket was destroyed, remove it from the free buffers too.
- if (!s.writable)
- sets.push(this.freeSockets);
-
- for (let sk = 0; sk < sets.length; sk++) {
- const sockets = sets[sk];
-
- if (sockets[name]) {
- const index = sockets[name].indexOf(s);
- if (index !== -1) {
- sockets[name].splice(index, 1);
- // Don't leak
- if (sockets[name].length === 0)
- delete sockets[name];
- }
- }
- }
-
- let req;
- if (this.requests[name]?.length) {
- debug('removeSocket, have a request, make a socket');
- req = this.requests[name][0];
- } else {
- // TODO(rickyes): this logic will not be FIFO across origins.
- // There might be older requests in a different origin, but
- // if the origin which releases the socket has pending requests
- // that will be prioritized.
- const keys = ObjectKeys(this.requests);
- for (let i = 0; i < keys.length; i++) {
- const prop = keys[i];
- // Check whether this specific origin is already at maxSockets
- if (this.sockets[prop]?.length) break;
- debug('removeSocket, have a request with different origin,' +
- ' make a socket');
- req = this.requests[prop][0];
- options = req[kRequestOptions];
- break;
- }
- }
-
- if (req && options) {
- req[kRequestOptions] = undefined;
- // If we have pending requests and a socket gets closed make a new one
- this.createSocket(req, options, (err, socket) => {
- if (err) {
- handleSocketAfterProxy(err, req);
- req.onSocket(null, err);
- return;
- }
-
- socket.emit('free');
- });
- }
-
-};
-
-Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
- socket.setKeepAlive(true, this.keepAliveMsecs);
- socket.unref();
-
- let agentTimeout = this.options.timeout || 0;
- let canKeepSocketAlive = true;
-
- if (socket._httpMessage?.res) {
- const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
-
- if (keepAliveHint) {
- const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];
-
- if (hint) {
- // Let the timer expire before the announced timeout to reduce
- // the likelihood of ECONNRESET errors
- let serverHintTimeout = (NumberParseInt(hint) * 1000) - this.agentKeepAliveTimeoutBuffer;
- serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
- if (serverHintTimeout === 0) {
- // Cannot safely reuse the socket because the server timeout is
- // too short
- canKeepSocketAlive = false;
- } else if (serverHintTimeout < agentTimeout) {
- agentTimeout = serverHintTimeout;
- }
- }
- }
- }
-
- if (socket.timeout !== agentTimeout) {
- socket.setTimeout(agentTimeout);
- }
-
- return canKeepSocketAlive;
-};
-
-Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
- debug('have free socket');
- socket.removeListener('error', freeSocketErrorListener);
- removeFreeSocketDataGuard(socket);
- req.reusedSocket = true;
- socket.ref();
-};
-
-Agent.prototype.destroy = function destroy() {
- const sets = [this.freeSockets, this.sockets];
- for (let s = 0; s < sets.length; s++) {
- const set = sets[s];
- const keys = ObjectKeys(set);
- for (let v = 0; v < keys.length; v++) {
- const setName = set[keys[v]];
- for (let n = 0; n < setName.length; n++) {
- setName[n].destroy();
- }
- }
- }
-};
-
-function setRequestSocket(agent, req, socket) {
- req.onSocket(socket);
- const agentTimeout = agent.options.timeout || 0;
- if (req.timeout === undefined || req.timeout === agentTimeout) {
- return;
- }
- socket.setTimeout(req.timeout);
-}
-
-function asyncResetHandle(socket) {
- // Guard against an uninitialized or user supplied Socket.
- const handle = socket._handle;
- if (handle && typeof handle.asyncReset === 'function') {
- // Assign the handle a new asyncId and run any destroy()/init() hooks.
- handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle));
- socket[async_id_symbol] = handle.getAsyncId();
- }
-}
+const { Agent, globalAgent } = require('internal/http/agent');
module.exports = {
Agent,
- globalAgent: getGlobalAgent(getOptionValue('--use-env-proxy') ? process.env : undefined, Agent),
+ globalAgent,
};
+
+process.emitWarning('The _http_agent module is deprecated. Use `node:http` instead.',
+ 'DeprecationWarning', 'DEP0199');
diff --git a/lib/_http_client.js b/lib/_http_client.js
index 21cf9e8d407..de13dc247aa 100644
--- a/lib/_http_client.js
+++ b/lib/_http_client.js
@@ -21,1283 +21,11 @@
'use strict';
-const {
- ArrayIsArray,
- Boolean,
- Error,
- NumberIsFinite,
- ObjectAssign,
- ObjectDefineProperty,
- ObjectHasOwn,
- ObjectKeys,
- ObjectSetPrototypeOf,
- ReflectApply,
- String,
- Symbol,
-} = primordials;
-
-const net = require('net');
-const assert = require('internal/assert');
-const {
- deprecateInstantiation,
- kEmptyObject,
- once,
-} = require('internal/util');
-const {
- _checkIsHttpToken: checkIsHttpToken,
- freeParser,
- parsers,
- HTTPParser,
- calculateLenientFlags,
- prepareError,
- kSkipPendingData,
-} = require('_http_common');
-const { kDetachAbortSignal } = require('_http_incoming');
-const {
- kHighWaterMark,
- kUniqueHeaders,
- parseUniqueHeadersOption,
- OutgoingMessage,
-} = require('_http_outgoing');
-const Agent = require('_http_agent');
-const { Buffer } = require('buffer');
-const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
-const { URL, urlToHttpOptions, isURL } = require('internal/url');
-const {
- kOutHeaders,
- kNeedDrain,
- isTraceHTTPEnabled,
- traceBegin,
- traceEnd,
- getNextTraceEventId,
- kProxyConfig,
- checkShouldUseProxy,
-} = require('internal/http');
-const {
- ConnResetException,
- codes: {
- ERR_HTTP_HEADERS_SENT,
- ERR_INVALID_ARG_TYPE,
- ERR_INVALID_ARG_VALUE,
- ERR_INVALID_HTTP_TOKEN,
- ERR_INVALID_PROTOCOL,
- ERR_UNESCAPED_CHARACTERS,
- },
-} = require('internal/errors');
-const {
- validateInteger,
- validateBoolean,
- validateOneOf,
- validatePort,
- validateString,
-} = require('internal/validators');
-const { getTimerDuration } = require('internal/timers');
-const {
- hasObserver,
- startPerf,
- stopPerf,
-} = require('internal/perf/observe');
-
-const kClientRequestStatistics = Symbol('ClientRequestStatistics');
-
-const dc = require('diagnostics_channel');
-const onClientRequestCreatedChannel = dc.channel('http.client.request.created');
-const onClientRequestStartChannel = dc.channel('http.client.request.start');
-const onClientRequestErrorChannel = dc.channel('http.client.request.error');
-const onClientResponseFinishChannel = dc.channel('http.client.response.finish');
-
-function emitErrorEvent(request, error) {
- if (onClientRequestErrorChannel.hasSubscribers) {
- onClientRequestErrorChannel.publish({
- request,
- error,
- });
- }
- request.emit('error', error);
-}
-
-const { addAbortSignal, finished } = require('stream');
-
-let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
- debug = fn;
-});
-
-const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
-const kError = Symbol('kError');
-const kPath = Symbol('kPath');
-const kAuthority = Symbol('kAuthority');
-const kProxyRewrittenToAbsolute = Symbol('kProxyRewrittenToAbsolute');
-
-const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request';
-
-function validateHost(host, name) {
- if (host !== null && host !== undefined && typeof host !== 'string') {
- throw new ERR_INVALID_ARG_TYPE(`options.${name}`,
- ['string', 'undefined', 'null'],
- host);
- }
- return host;
-}
-
-class HTTPClientAsyncResource {
- constructor(type, req) {
- this.type = type;
- this.req = req;
- }
-}
-
-// The only documented shape is [k, v, k, v, ...]. Here we also accept [[k, v], [k, v], ...].
-// for backward compatibility, and reject others. Also reject if there are duplicate Host entries.
-// Returns the Host header value, or undefined if absent.
-function getHostFromHeaderArray(headers) {
- let host;
- const isPairs = headers.length > 0 && ArrayIsArray(headers[0]);
- if (isPairs) {
- for (let i = 0; i < headers.length; i++) {
- const entry = headers[i];
- if (!ArrayIsArray(entry)) {
- throw new ERR_INVALID_ARG_VALUE(`options.headers[${i}]`, typeof entry,
- 'must be an array when headers is passed as an array of pairs');
- }
- if (`${entry[0]}`.toLowerCase() === 'host') {
- if (host !== undefined) {
- throw new ERR_INVALID_ARG_VALUE('options.headers', '(redacted)',
- 'must not contain duplicate Host headers');
- }
- host = `${entry[1]}`;
- }
- }
- } else {
- for (let i = 0; i + 1 < headers.length; i += 2) {
- if (`${headers[i]}`.toLowerCase() === 'host') {
- if (host !== undefined) {
- throw new ERR_INVALID_ARG_VALUE('options.headers', '(redacted)',
- 'must not contain duplicate Host headers');
- }
- host = `${headers[i + 1]}`;
- }
- }
- }
- return host;
-}
-
-function authoritiesMatch(canonicalHost, hostFromHeader) {
- let parsed;
- try {
- parsed = new URL(`http://${hostFromHeader}`);
- } catch {
- return false;
- }
- if (parsed.username || parsed.password ||
- parsed.pathname !== '/' || parsed.search || parsed.hash) {
- return false;
- }
- return parsed.host === canonicalHost;
-}
-
-// https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
-// When the request target is in absolute-form, ensure it is consistent with
-// the request authority: same scheme, no userinfo, and an authority
-// component agree with options.host[:port].
-function validateRequestAuthority(pathOption, proxyAuthority, userHostHeader, headerArray) {
- validatePort(proxyAuthority.port, 'options.port', true);
- pathOption = `${pathOption}`;
- const requestBase = new URL(`http://${proxyAuthority.host}`);
- requestBase.port = proxyAuthority.port;
-
- const result = { requestBase };
- if (headerArray !== undefined) {
- const host = getHostFromHeaderArray(headerArray);
- // Since we don't mutate the header array to normalize the Host value, unlike
- // in the case of other shapes of headers provided, we check that it is identical
- // to the authority from the requestBase.
- if (host !== undefined && host !== requestBase.host) {
- throw new ERR_INVALID_ARG_VALUE(
- 'Host in options.headers', host,
- `must match the request authority (${requestBase.host})`);
- }
- } else if (userHostHeader !== undefined) {
- if (!authoritiesMatch(requestBase.host, userHostHeader)) {
- throw new ERR_INVALID_ARG_VALUE(
- 'Host in options.headers', userHostHeader,
- `must match the request authority (${requestBase.host})`);
- }
- }
-
- // Per RFC 9112 Section 3.2, if request target is in absolute-form its authority
- // must agree with the request authority.
- let requestURL;
- let isAbsoluteForm = false;
- try {
- requestURL = new URL(pathOption);
- isAbsoluteForm = true;
- } catch {
- if (pathOption.charCodeAt(0) !== 0x2F) {
- throw new ERR_INVALID_ARG_VALUE(
- 'options.path', pathOption, 'must be in absolute-form or start with /');
- }
- requestURL = new URL(requestBase.origin + pathOption);
- }
- result.requestURL = requestURL;
- if (!isAbsoluteForm) {
- return result;
- }
-
- if (requestURL.username || requestURL.password) {
- requestURL.username = '';
- requestURL.password = '';
- throw new ERR_INVALID_ARG_VALUE(
- 'options.path', requestURL.href, 'must not contain userinfo, use options.auth instead');
- }
-
- if (requestURL.protocol !== 'http:') {
- throw new ERR_INVALID_ARG_VALUE(
- 'options.path', requestURL.protocol, 'must use http: scheme when specified as an absolute URL');
- }
-
- if (requestBase.host !== requestURL.host) {
- throw new ERR_INVALID_ARG_VALUE(
- 'options.path', requestURL, `must match the request authority (${requestBase.host})`);
- }
-
- return result;
-}
-
-// When proxying a HTTP request, the following needs to be done:
-// https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.2
-// 1. Rewrite the request path to absolute-form.
-// 2. Add proxy-connection and proxy-authorization headers appropriately.
-//
-// This function checks whether the request should be rewritten for proxying
-// and modifies the headers as well as req.path if necessary.
-// The handling of the proxy server connection is done in createConnection.
-// It also validates that the Host header and absolute-form path authority match the
-// request authority specified by reqOptions.
-function rewriteForProxiedHttp(req, reqOptions, proxyAuthority, userHostHeader, headerArray) {
- if (req._header) {
- debug('request._header is already sent, skipping rewriteForProxiedHttp', reqOptions);
- return false;
- }
- const agent = req.agent;
- if (!agent || !agent[kProxyConfig]) {
- return false;
- }
- if ((reqOptions.protocol || agent.protocol) !== 'http:') {
- return false;
- }
- // TODO(joyeecheung): cache this on the req or on the agent.
- const shouldUseProxy = checkShouldUseProxy(agent[kProxyConfig], reqOptions);
- debug(`rewriteForProxiedHttp should use proxy for ${reqOptions.host}:${reqOptions.port}:`, shouldUseProxy);
- if (!shouldUseProxy) {
- return false;
- }
-
- // Per RFC 9112 Section 3.2.2, we don't need to rewrite CONNECT or OPTIONS * requests.
- let requestURL;
- if (req.method !== 'CONNECT' && !(req.method === 'OPTIONS' && req.path === '*')) {
- // Validate Host header values agree with the request authority before mutating req,
- // so a rejected request doesn't leave proxy-* headers stuck on the outgoing header store.
- // XXX(joyeecheung): This validates whether the request conforms to the RFC, but here
- // we only do it for proxied requests for backward compatibility. For non-proxied requests,
- // ensuring that the request is well formed has been entirely left to the user.
- const result = validateRequestAuthority(req.path, proxyAuthority, userHostHeader, headerArray);
- if (headerArray === undefined) {
- const currentHost = req.getHeader('host');
- if (currentHost !== undefined && currentHost !== result.requestBase.host) {
- req.setHeader('Host', result.requestBase.host);
- }
- }
- requestURL = result.requestURL;
- }
-
- // Add proxy headers.
- const { auth, href } = agent[kProxyConfig];
- if (auth) {
- req.setHeader('proxy-authorization', auth);
- }
- if (req.shouldKeepAlive) {
- req.setHeader('proxy-connection', 'keep-alive');
- } else {
- req.setHeader('proxy-connection', 'close');
- }
-
- if (requestURL !== undefined) {
- // Convert the path to absolute-form. The authority is built from options.
- req.path = requestURL.href;
- req[kProxyRewrittenToAbsolute] = true;
- }
- debug(`updated request for HTTP proxy ${href} with ${req.path} `, req[kOutHeaders]);
- return true;
-};
-
-function ClientRequest(input, options, cb) {
- if (!(this instanceof ClientRequest)) {
- return deprecateInstantiation(ClientRequest, 'DEP0195', input, options, cb);
- }
-
- OutgoingMessage.call(this);
-
- let pathIsFromURL = false;
- if (typeof input === 'string') {
- const urlStr = input;
- input = urlToHttpOptions(new URL(urlStr));
- pathIsFromURL = true;
- } else if (isURL(input)) {
- // url.URL instance
- input = urlToHttpOptions(input);
- pathIsFromURL = true;
- } else {
- cb = options;
- options = input;
- input = null;
- }
-
- if (typeof options === 'function') {
- cb = options;
- options = input || kEmptyObject;
- } else {
- const hasPathOverride = pathIsFromURL &&
- options != null &&
- ObjectHasOwn(options, 'path');
- if (hasPathOverride) {
- pathIsFromURL = false;
- }
-
- options = ObjectAssign({ __proto__: null }, input, options);
- }
-
- // Propagate the user's highWaterMark to OutgoingMessage so that
- // _writeRaw() uses the correct threshold for writes buffered before
- // the socket connects (Path B). Without this, the OutgoingMessage
- // defaults to 64 KB regardless of what the caller requested.
- if (options.highWaterMark != null) {
- this[kHighWaterMark] = options.highWaterMark;
- }
-
- let agent = options.agent;
- const defaultAgent = options._defaultAgent || Agent.globalAgent;
- if (agent === false) {
- agent = new defaultAgent.constructor();
- } else if (agent === null || agent === undefined) {
- if (typeof options.createConnection !== 'function') {
- agent = defaultAgent;
- }
- // Explicitly pass through this statement as agent will not be used
- // when createConnection is provided.
- } else if (typeof agent.addRequest !== 'function') {
- throw new ERR_INVALID_ARG_TYPE('options.agent',
- ['Agent-like Object', 'undefined', 'false'],
- agent);
- }
- this.agent = agent;
-
- const protocol = options.protocol || defaultAgent.protocol;
- let expectedProtocol = defaultAgent.protocol;
- if (this.agent?.protocol)
- expectedProtocol = this.agent.protocol;
-
- if (options.path) {
- const path = String(options.path);
- if (INVALID_PATH_REGEX.test(path)) {
- debug('Path contains unescaped characters: "%s"', path);
- throw new ERR_UNESCAPED_CHARACTERS('Request path');
- }
- }
-
- if (protocol !== expectedProtocol) {
- throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol);
- }
-
- const defaultPort = options.defaultPort ||
- (this.agent?.defaultPort);
-
- const optsWithoutSignal = { __proto__: null, ...options };
-
- const port = optsWithoutSignal.port = options.port || defaultPort || 80;
- const host = optsWithoutSignal.host = validateHost(options.hostname, 'hostname') ||
- validateHost(options.host, 'host') || 'localhost';
-
- const setHost = options.setHost !== undefined ?
- Boolean(options.setHost) :
- options.setDefaultHeaders !== false;
-
- this._removedConnection = options.setDefaultHeaders === false;
- this._removedContLen = options.setDefaultHeaders === false;
- this._removedTE = options.setDefaultHeaders === false;
-
- this.socketPath = options.socketPath;
-
- if (options.timeout !== undefined)
- this.timeout = getTimerDuration(options.timeout, 'timeout');
-
- const signal = options.signal;
- if (signal) {
- addAbortSignal(signal, this);
- delete optsWithoutSignal.signal;
- }
- let method = options.method;
- if (method != null) {
- validateString(method, 'options.method');
- }
-
- if (method) {
- if (!checkIsHttpToken(method)) {
- throw new ERR_INVALID_HTTP_TOKEN('Method', method);
- }
- method = this.method = method.toUpperCase();
- } else {
- method = this.method = 'GET';
- }
-
- const maxHeaderSize = options.maxHeaderSize;
- if (maxHeaderSize !== undefined)
- validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
- this.maxHeaderSize = maxHeaderSize;
-
- const insecureHTTPParser = options.insecureHTTPParser;
- if (insecureHTTPParser !== undefined) {
- validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
- }
-
- this.insecureHTTPParser = insecureHTTPParser;
-
- const httpValidation = options.httpValidation;
- if (httpValidation !== undefined) {
- validateOneOf(httpValidation, 'options.httpValidation',
- ['strict', 'relaxed', 'insecure']);
- if (insecureHTTPParser !== undefined) {
- throw new ERR_INVALID_ARG_VALUE(
- 'options.httpValidation',
- httpValidation,
- 'cannot be used together with options.insecureHTTPParser',
- );
- }
- }
-
- this.httpValidation = httpValidation;
-
- if (options.joinDuplicateHeaders !== undefined) {
- validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders');
- }
-
- this.joinDuplicateHeaders = options.joinDuplicateHeaders;
-
- let path = options.path || '/';
- // Strip the leading slash added when the CONNECT target comes from a URL.
- if (method === 'CONNECT' && pathIsFromURL && path[0] === '/') {
- path = path.slice(1) || '/';
- }
-
- this[kPath] = path;
- if (cb) {
- this.once('response', cb);
- }
-
- if (method === 'GET' ||
- method === 'HEAD' ||
- method === 'DELETE' ||
- method === 'OPTIONS' ||
- method === 'TRACE' ||
- method === 'CONNECT') {
- this.useChunkedEncodingByDefault = false;
- } else {
- this.useChunkedEncodingByDefault = true;
- }
-
- this._ended = false;
- this.res = null;
- this.aborted = false;
- this.timeoutCb = null;
- this.upgradeOrConnect = false;
- this.parser = null;
- this.maxHeadersCount = null;
- this.reusedSocket = false;
- this.host = host;
- this.protocol = protocol;
- this[kProxyRewrittenToAbsolute] = false;
-
- if (this.agent) {
- // If there is an agent we should default to Connection:keep-alive,
- // but only if the Agent will actually reuse the connection!
- // If it's not a keepAlive agent, and the maxSockets==Infinity, then
- // there's never a case where this socket will actually be reused
- if (!this.agent.keepAlive && !NumberIsFinite(this.agent.maxSockets)) {
- this._last = true;
- this.shouldKeepAlive = false;
- } else {
- this._last = false;
- this.shouldKeepAlive = true;
- }
- }
-
- let hostHeaderFromOptions = host;
- // For the Host header, ensure that IPv6 addresses are enclosed
- // in square brackets, as defined by URI formatting
- // https://tools.ietf.org/html/rfc3986#section-3.2.2
- const posColon = hostHeaderFromOptions.indexOf(':');
- if (posColon !== -1 &&
- hostHeaderFromOptions.includes(':', posColon + 1) &&
- hostHeaderFromOptions.charCodeAt(0) !== 91/* '[' */) {
- hostHeaderFromOptions = `[${hostHeaderFromOptions}]`;
- }
- const proxyAuthority = { host: hostHeaderFromOptions, port };
-
- if (port && +port !== defaultPort) {
- hostHeaderFromOptions += ':' + port;
- }
- // Preserve the request authority (with the port when non-default) so that
- // the perf_hooks entry can report a faithful URL.
- this[kAuthority] = hostHeaderFromOptions;
- const headersArray = ArrayIsArray(options.headers);
- if (!headersArray) {
- if (options.headers) {
- const keys = ObjectKeys(options.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];
- this.setHeader(key, options.headers[key]);
- }
- }
-
- // Save the Host header before the implicit auto-set below, so the
- // proxy validator can tell user-explicit values from Node-generated ones.
- const userHostHeader = this.getHeader('host');
-
- if (host && !this.getHeader('host') && setHost) {
- const hostHeader = method === 'CONNECT' && options.path ?
- String(this.path) : hostHeaderFromOptions;
- this.setHeader('Host', hostHeader);
- }
-
- if (options.auth && !this.getHeader('Authorization')) {
- this.setHeader('Authorization', 'Basic ' +
- Buffer.from(options.auth).toString('base64'));
- }
-
- if (this.getHeader('expect')) {
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT('render');
- }
-
- rewriteForProxiedHttp(this, optsWithoutSignal, proxyAuthority, userHostHeader);
- this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
- this[kOutHeaders]);
- } else {
- rewriteForProxiedHttp(this, optsWithoutSignal, proxyAuthority, userHostHeader);
- }
- } else {
- rewriteForProxiedHttp(this, optsWithoutSignal, proxyAuthority, undefined, options.headers);
- this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
- options.headers);
- }
-
- this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
-
- // initiate connection
- if (this.agent) {
- this.agent.addRequest(this, optsWithoutSignal);
- } else {
- // No agent, default to Connection:close.
- this._last = true;
- this.shouldKeepAlive = false;
- let opts = optsWithoutSignal;
- if (opts.path || opts.socketPath) {
- opts = { ...optsWithoutSignal };
- if (opts.socketPath) {
- opts.path = opts.socketPath;
- } else {
- opts.path &&= undefined;
- }
- }
- if (typeof opts.createConnection === 'function') {
- const oncreate = once((err, socket) => {
- if (err) {
- process.nextTick(() => emitErrorEvent(this, err));
- } else {
- this.onSocket(socket);
- }
- });
-
- try {
- const newSocket = opts.createConnection(opts, oncreate);
- if (newSocket) {
- oncreate(null, newSocket);
- }
- } catch (err) {
- oncreate(err);
- }
- } else {
- debug('CLIENT use net.createConnection', opts);
- this.onSocket(net.createConnection(opts));
- }
- }
- if (onClientRequestCreatedChannel.hasSubscribers) {
- onClientRequestCreatedChannel.publish({
- request: this,
- });
- }
-}
-ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype);
-ObjectSetPrototypeOf(ClientRequest, OutgoingMessage);
-
-ObjectDefineProperty(ClientRequest.prototype, 'path', {
- __proto__: null,
- get() {
- return this[kPath];
- },
- set(value) {
- const path = String(value);
- if (INVALID_PATH_REGEX.test(path)) {
- throw new ERR_UNESCAPED_CHARACTERS('Request path');
- }
- this[kPath] = path;
- },
- configurable: true,
- enumerable: true,
-});
-
-ClientRequest.prototype._finish = function _finish() {
- OutgoingMessage.prototype._finish.call(this);
- if (hasObserver('http')) {
- startPerf(this, kClientRequestStatistics, {
- type: 'http',
- name: 'HttpClient',
- detail: {
- req: {
- method: this.method,
- // If the path has been rewritten to absolute-form for proxying,
- // it is already a full URL.
- url: this[kProxyRewrittenToAbsolute] ?
- this.path :
- `${this.protocol}//${this[kAuthority]}${this.path}`,
- headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {},
- },
- },
- });
- }
- if (onClientRequestStartChannel.hasSubscribers) {
- onClientRequestStartChannel.publish({
- request: this,
- });
- }
- if (isTraceHTTPEnabled()) {
- this._traceEventId = getNextTraceEventId();
- traceBegin(HTTP_CLIENT_TRACE_EVENT_NAME, this._traceEventId);
- }
-};
-
-ClientRequest.prototype._implicitHeader = function _implicitHeader() {
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT('render');
- }
- this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
- this[kOutHeaders]);
-};
-
-ClientRequest.prototype.abort = function abort() {
- if (this.aborted) {
- return;
- }
- this.aborted = true;
- process.nextTick(emitAbortNT, this);
- this.destroy();
-};
-
-ClientRequest.prototype.destroy = function destroy(err) {
- if (this.destroyed) {
- return this;
- }
- this.destroyed = true;
-
- // If we're aborting, we don't care about any more response data.
- if (this.res) {
- this.res._dump();
- }
-
- // Nothing left to cancel: the request was fully sent and the response was
- // fully received. Destroying the socket here would emit an error on a
- // socket that is already being released to the agent, at which point
- // socketErrorListener has been removed and nothing would handle it.
- if (this.writableFinished && this.res?.complete) {
- return this;
- }
-
- this[kError] = err;
- this.socket?.destroy(err);
-
- return this;
-};
-
-function emitAbortNT(req) {
- req.emit('abort');
-}
-
-function ondrain() {
- const msg = this._httpMessage;
- if (msg && !msg.finished && msg[kNeedDrain]) {
- msg[kNeedDrain] = false;
- msg.emit('drain');
- }
-}
-
-function socketCloseListener() {
- const socket = this;
- const req = socket._httpMessage;
- debug('HTTP socket close');
-
- // NOTE: It's important to get parser here, because it could be freed by
- // the `socketOnData`.
- const parser = socket.parser;
- const res = req.res;
-
- req.destroyed = true;
- if (res) {
- // Socket closed before we emitted 'end' below.
- if (!res.complete) {
- res.destroy(new ConnResetException('aborted',
- req[kError] ? { cause: req[kError] } : undefined));
- }
- req._closed = true;
- req.emit('close');
- if (!res.aborted && res.readable) {
- res.push(null);
- }
- } else {
- if (!req.socket._hadError) {
- // This socket error fired before we started to
- // receive a response. The error needs to
- // fire on the request.
- req.socket._hadError = true;
- emitErrorEvent(req, new ConnResetException('socket hang up'));
- }
- req._closed = true;
- req.emit('close');
- }
-
- // Too bad. That output wasn't getting written.
- // This is pretty terrible that it doesn't raise an error.
- // Fixed better in v0.10
- if (req.outputData)
- req.outputData.length = 0;
-
- if (parser) {
- parser.finish();
- freeParser(parser, req, socket);
- }
-}
-
-function socketErrorListener(err) {
- const socket = this;
- const req = socket._httpMessage;
- debug('SOCKET ERROR:', err.message, err.stack);
-
- if (req) {
- // For Safety. Some additional errors might fire later on
- // and we need to make sure we don't double-fire the error event.
- socket._hadError = true;
- emitErrorEvent(req, err);
- // Preserve the socket error for an incomplete response, including TLS
- // errors that are emitted without setting socket.errored.
- req[kError] ||= err;
- }
-
- const parser = socket.parser;
- if (parser) {
- parser.finish();
- freeParser(parser, req, socket);
- }
-
- // Ensure that no further data will come out of the socket
- socket.removeListener('data', socketOnData);
- socket.removeListener('end', socketOnEnd);
- socket.destroy();
-}
-
-function socketOnEnd() {
- const socket = this;
- const req = this._httpMessage;
- const parser = this.parser;
-
- if (!req.res && !req.socket._hadError) {
- // If we don't have a response then we know that the socket
- // ended prematurely and we need to emit an error on the request.
- req.socket._hadError = true;
- emitErrorEvent(req, new ConnResetException('socket hang up'));
- }
- if (parser) {
- parser.finish();
- freeParser(parser, req, socket);
- }
- socket.destroy();
-}
-
-function socketOnData(d) {
- const socket = this;
- const req = this._httpMessage;
- const parser = this.parser;
-
- assert(parser && parser.socket === socket);
-
- const ret = parser.execute(d);
- if (ret instanceof Error) {
- prepareError(ret, parser, d);
- debug('parse error', ret);
- freeParser(parser, req, socket);
- socket.removeListener('data', socketOnData);
- socket.removeListener('end', socketOnEnd);
- socket.destroy();
- req.socket._hadError = true;
- emitErrorEvent(req, ret);
- } else if (parser.incoming?.upgrade) {
- // Upgrade (if status code 101) or CONNECT
- const bytesParsed = ret;
- const res = parser.incoming;
- req.res = res;
-
- socket.removeListener('data', socketOnData);
- socket.removeListener('end', socketOnEnd);
- socket.removeListener('drain', ondrain);
-
- if (req.timeoutCb) socket.removeListener('timeout', req.timeoutCb);
- socket.removeListener('timeout', responseOnTimeout);
-
- parser.finish();
- freeParser(parser, req, socket);
-
- const bodyHead = d.slice(bytesParsed, d.length);
-
- const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
- if (req.listenerCount(eventName) > 0) {
- req.upgradeOrConnect = true;
-
- // detach the socket
- socket.emit('agentRemove');
- socket.removeListener('close', socketCloseListener);
- socket.removeListener('error', socketErrorListener);
-
- socket._httpMessage = null;
- socket.readableFlowing = null;
-
- req.emit(eventName, res, socket, bodyHead);
- req.destroyed = true;
- req._closed = true;
- req.emit('close');
- } else {
- // Requested Upgrade or used CONNECT method, but have no handler.
- socket.destroy();
- }
- } else if (parser.incoming?.complete &&
- // When the status code is informational (100, 102-199),
- // the server will send a final response after this client
- // sends a request body, so we must not free the parser.
- // 101 (Switching Protocols) and all other status codes
- // should be processed normally.
- !statusIsInformational(parser.incoming.statusCode)) {
- socket.removeListener('data', socketOnData);
- socket.removeListener('end', socketOnEnd);
- socket.removeListener('drain', ondrain);
- freeParser(parser, req, socket);
- }
-}
-
-function statusIsInformational(status) {
- // 100 (Continue) RFC7231 Section 6.2.1
- // 102 (Processing) RFC2518
- // 103 (Early Hints) RFC8297
- // 104-199 (Unassigned)
- return (status < 200 && status >= 100 && status !== 101);
-}
-
-// client
-function parserOnIncomingClient(res, shouldKeepAlive) {
- const socket = this.socket;
- const req = socket._httpMessage;
-
- debug('AGENT incoming response!');
-
- if (req.res) {
- // We already have a response object, this means the server
- // sent a double response.
- socket.destroy();
- if (socket.parser) {
- // https://github.com/nodejs/node/issues/60025
- // Now, parser.incoming is pointed to the new IncomingMessage,
- // we need to rewrite it to the first one and skip all the pending IncomingMessage
- socket.parser.incoming = req.res;
- socket.parser.incoming[kSkipPendingData] = true;
- }
- return 0;
- }
- req.res = res;
-
- // Skip body and treat as Upgrade.
- if (res.upgrade)
- return 2;
-
- // Responses to CONNECT request is handled as Upgrade.
- const method = req.method;
- if (method === 'CONNECT') {
- res.upgrade = true;
- return 2; // Skip body and treat as Upgrade.
- }
-
- if (statusIsInformational(res.statusCode)) {
- // Restart the parser, as this is a 1xx informational message.
- req.res = null; // Clear res so that we don't hit double-responses.
- // Maintain compatibility by sending 100-specific events
- if (res.statusCode === 100) {
- req.emit('continue');
- }
- // Send information events to all 1xx responses except 101 Upgrade.
- req.emit('information', {
- statusCode: res.statusCode,
- statusMessage: res.statusMessage,
- httpVersion: res.httpVersion,
- httpVersionMajor: res.httpVersionMajor,
- httpVersionMinor: res.httpVersionMinor,
- headers: res.headers,
- rawHeaders: res.rawHeaders,
- });
-
- return 1; // Skip body but don't treat as Upgrade.
- }
-
- if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) {
- // Server MUST respond with Connection:keep-alive for us to enable it.
- // If we've been upgraded (via WebSockets) we also shouldn't try to
- // keep the connection open.
- req.shouldKeepAlive = false;
- }
-
- if (req[kClientRequestStatistics] && hasObserver('http')) {
- stopPerf(req, kClientRequestStatistics, {
- detail: {
- res: {
- statusCode: res.statusCode,
- statusMessage: res.statusMessage,
- headers: res.headers,
- },
- },
- });
- }
- if (onClientResponseFinishChannel.hasSubscribers) {
- onClientResponseFinishChannel.publish({
- request: req,
- response: res,
- });
- }
- if (isTraceHTTPEnabled() && typeof req._traceEventId === 'number') {
- traceEnd(HTTP_CLIENT_TRACE_EVENT_NAME, req._traceEventId, {
- path: req.path,
- statusCode: res.statusCode,
- });
- }
- req.res = res;
- res.req = req;
-
- // Add our listener first, so that we guarantee socket cleanup
- res.on('end', responseOnEnd);
- req.on('finish', requestOnFinish);
- socket.on('timeout', responseOnTimeout);
-
- // If the user did not listen for the 'response' event, then they
- // can't possibly read the data, so we ._dump() it into the void
- // so that the socket doesn't hang there in a paused state.
- if (req.aborted || !req.emit('response', res))
- res._dump();
-
- if (method === 'HEAD')
- return 1; // Skip body but don't treat as Upgrade.
-
- if (res.statusCode === 304) {
- res.complete = true;
- return 1; // Skip body as there won't be any
- }
-
- return 0; // No special treatment.
-}
-
-// client
-function responseKeepAlive(req) {
- const socket = req.socket;
-
- debug('AGENT socket keep-alive');
- if (req.timeoutCb) {
- socket.setTimeout(0, req.timeoutCb);
- req.timeoutCb = null;
- }
- socket.removeListener('close', socketCloseListener);
- socket.removeListener('error', socketErrorListener);
- socket.removeListener('data', socketOnData);
- socket.removeListener('end', socketOnEnd);
-
- // TODO(ronag): Between here and emitFreeNT the socket
- // has no 'error' handler.
-
- // There are cases where _handle === null. Avoid those. Passing undefined to
- // nextTick() will call getDefaultTriggerAsyncId() to retrieve the id.
- const asyncId = socket._handle ? socket._handle.getAsyncId() : undefined;
- // Mark this socket as available, AFTER user-added end
- // handlers have a chance to run.
- defaultTriggerAsyncIdScope(asyncId, process.nextTick, emitFreeNT, req);
-
- req.destroyed = true;
- if (req.res) {
- // Detach socket from IncomingMessage to avoid destroying the freed
- // socket in IncomingMessage.destroy().
- req.res.socket = null;
- }
-}
-
-function responseOnEnd() {
- const req = this.req;
- const socket = req.socket;
-
- this[kDetachAbortSignal]();
-
- if (socket) {
- if (req.timeoutCb) socket.removeListener('timeout', emitRequestTimeout);
- socket.removeListener('timeout', responseOnTimeout);
- }
-
- req._ended = true;
-
- if (!req.shouldKeepAlive) {
- if (socket.writable) {
- debug('AGENT socket.destroySoon()');
- if (typeof socket.destroySoon === 'function')
- socket.destroySoon();
- else
- socket.end();
- }
- assert(!socket.writable);
- } else if (req.writableFinished && !this.aborted) {
- assert(req.finished);
- // We can assume `req.finished` means all data has been written since:
- // - `'responseOnEnd'` means we have been assigned a socket.
- // - when we have a socket we write directly to it without buffering.
- // - `req.finished` means `end()` has been called and no further data.
- // can be written
- // In addition, `req.writableFinished` means all data written has been
- // accepted by the kernel. (i.e. the `req.socket` is drained).Without
- // this constraint, we may assign a non drained socket to a request.
- responseKeepAlive(req);
- }
-}
-
-function responseOnTimeout() {
- const req = this._httpMessage;
- if (!req) return;
- const res = req.res;
- if (!res) return;
- res.emit('timeout');
-}
-
-// This function is necessary in the case where we receive the entire response
-// from the server before we finish sending out the request.
-function requestOnFinish() {
- const req = this;
-
- // If the response ends before this request finishes writing, `responseOnEnd()`
- // already released the socket. When `finish` fires later, that socket may
- // belong to a different request, so only call `responseKeepAlive()` when the
- // original request is still alive (`!req.destroyed`).
- if (req.shouldKeepAlive && req._ended && !req.destroyed)
- responseKeepAlive(req);
-}
-
-function emitFreeNT(req) {
- req._closed = true;
- req.emit('close');
- if (req.socket) {
- req.socket.emit('free');
- }
-}
-
-function tickOnSocket(req, socket) {
- const parser = parsers.alloc();
- req.socket = socket;
- const lenientFlags = calculateLenientFlags(req.httpValidation, req.insecureHTTPParser);
- // Propagate headers limit from request object to parser
- if (typeof req.maxHeadersCount === 'number') {
- parser.maxHeaderPairs = req.maxHeadersCount << 1;
- }
- parser.initialize(HTTPParser.RESPONSE,
- new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req),
- req.maxHeaderSize || 0,
- lenientFlags,
- undefined,
- parser.maxHeaderPairs);
- parser.socket = socket;
- parser.outgoing = req;
- req.parser = parser;
-
- socket.parser = parser;
- socket._httpMessage = req;
-
- parser.joinDuplicateHeaders = req.joinDuplicateHeaders;
-
- parser.onIncoming = parserOnIncomingClient;
- socket.on('data', socketOnData);
- socket.on('end', socketOnEnd);
- socket.on('close', socketCloseListener);
- socket.on('drain', ondrain);
-
- if (
- req.timeout !== undefined ||
- (req.agent?.options?.timeout)
- ) {
- listenSocketTimeout(req);
- }
- req.emit('socket', socket);
-}
-
-function emitRequestTimeout() {
- const req = this._httpMessage;
- if (req) {
- req.emit('timeout');
- }
-}
-
-function listenSocketTimeout(req) {
- if (req.timeoutCb) {
- return;
- }
- // Set timeoutCb so it will get cleaned up on request end.
- req.timeoutCb = emitRequestTimeout;
- // Delegate socket timeout event.
- if (req.socket) {
- req.socket.once('timeout', emitRequestTimeout);
- } else {
- req.on('socket', (socket) => {
- socket.once('timeout', emitRequestTimeout);
- });
- }
-}
-
-ClientRequest.prototype.onSocket = function onSocket(socket, err) {
- // Attach the error listener synchronously so that any errors emitted on
- // the socket before onSocketNT runs (e.g. from a blocklist check or other
- // next-tick error) are forwarded to the request and can be caught by the
- // user's error handler. socketErrorListener requires socket._httpMessage
- // to be set so we set it here too.
- if (socket && !err) {
- socket._httpMessage = this;
- socket.on('error', socketErrorListener);
- }
- process.nextTick(onSocketNT, this, socket, err);
-};
-
-function onSocketNT(req, socket, err) {
- if (req.destroyed || err) {
- req.destroyed = true;
-
- function _destroy(req, err) {
- if (!req.aborted && !err) {
- err = new ConnResetException('socket hang up');
- }
- // ERR_PROXY_TUNNEL is handled by the proxying logic.
- // Skip if the error was already emitted by the early socketErrorListener.
- if (err && err.code !== 'ERR_PROXY_TUNNEL' &&
- !socket?._hadError) {
- emitErrorEvent(req, err);
- }
- req._closed = true;
- req.emit('close');
- }
-
- if (socket) {
- if (!err && req.agent && !socket.destroyed) {
- socket.emit('free');
- socket.removeListener('error', socketErrorListener);
- } else {
- finished(socket.destroy(err || req[kError]), (er) => {
- if (er?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
- er = null;
- }
- _destroy(req, er || err);
- });
- return;
- }
- }
-
- _destroy(req, err || req[kError]);
- } else {
- tickOnSocket(req, socket);
- req._flush();
- }
-}
-
-ClientRequest.prototype._deferToConnect = _deferToConnect;
-function _deferToConnect(method, arguments_) {
- // This function is for calls that need to happen once the socket is
- // assigned to this request and writable. It's an important promisy
- // thing for all the socket calls that happen either now
- // (when a socket is assigned) or in the future (when a socket gets
- // assigned out of the pool and is eventually writable).
-
- const callSocketMethod = () => {
- if (method)
- ReflectApply(this.socket[method], this.socket, arguments_);
- };
-
- const onSocket = () => {
- if (this.socket.writable) {
- callSocketMethod();
- } else {
- this.socket.once('connect', callSocketMethod);
- }
- };
-
- if (!this.socket) {
- this.once('socket', onSocket);
- } else {
- onSocket();
- }
-}
-
-ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
- if (this._ended) {
- return this;
- }
-
- listenSocketTimeout(this);
- msecs = getTimerDuration(msecs, 'msecs');
- if (callback) this.once('timeout', callback);
-
- if (this.socket) {
- setSocketTimeout(this.socket, msecs);
- } else {
- this.once('socket', (sock) => setSocketTimeout(sock, msecs));
- }
-
- return this;
-};
-
-function setSocketTimeout(sock, msecs) {
- if (sock.connecting) {
- sock.once('connect', function() {
- sock.setTimeout(msecs);
- });
- } else {
- sock.setTimeout(msecs);
- }
-}
-
-ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
- this._deferToConnect('setNoDelay', [noDelay]);
-};
-
-ClientRequest.prototype.setSocketKeepAlive =
- function setSocketKeepAlive(enable, initialDelay) {
- this._deferToConnect('setKeepAlive', [enable, initialDelay]);
- };
-
-ClientRequest.prototype.clearTimeout = function clearTimeout(cb) {
- this.setTimeout(0, cb);
-};
+const { ClientRequest } = require('internal/http/client');
module.exports = {
ClientRequest,
};
+
+process.emitWarning('The _http_client module is deprecated. Use `node:http` instead.',
+ 'DeprecationWarning', 'DEP0199');
diff --git a/lib/_http_common.js b/lib/_http_common.js
index d5e7bdedee3..59b08153d13 100644
--- a/lib/_http_common.js
+++ b/lib/_http_common.js
@@ -22,317 +22,29 @@
'use strict';
const {
- MathMin,
- Symbol,
- Uint8Array,
-} = primordials;
-const { setImmediate } = require('timers');
-
-const { methods, allMethods, HTTPParser } = internalBinding('http_parser');
-const { getOptionValue } = require('internal/options');
-const insecureHTTPParser = getOptionValue('--insecure-http-parser');
-
-const FreeList = require('internal/freelist');
-const incoming = require('_http_incoming');
-const {
- IncomingMessage,
- readStart,
- readStop,
-} = incoming;
-
-const kIncomingMessage = Symbol('IncomingMessage');
-const kSkipPendingData = Symbol('SkipPendingData');
-const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;
-const kOnHeaders = HTTPParser.kOnHeaders | 0;
-const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
-const kOnBody = HTTPParser.kOnBody | 0;
-const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
-const kOnExecute = HTTPParser.kOnExecute | 0;
-const kOnTimeout = HTTPParser.kOnTimeout | 0;
-
-const MAX_HEADER_PAIRS = 2000;
-
-// Only called in the slow case where slow means
-// that the request headers were either fragmented
-// across multiple TCP packets or too large to be
-// processed in a single run. This method is also
-// called to process trailing HTTP headers.
-function parserOnHeaders(headers, url) {
- // Once we exceeded headers limit - stop collecting them
- const capacity = this.maxHeaderPairs - this._headers.length;
- if (this.maxHeaderPairs <= 0 || capacity >= headers.length) {
- this._headers.push(...headers);
- } else if (capacity > 0) {
- this._headers.push(...headers.slice(0, capacity));
- }
- this._url += url;
-}
-
-const HTTP_VERSION_1_1 = '1.1';
-
-// `headers` and `url` are set only if .onHeaders() has not been called for
-// this request.
-// `url` is not set for response parsers but that's not applicable here since
-// all our parsers are request parsers.
-function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
- url, statusCode, statusMessage, upgrade,
- shouldKeepAlive) {
- const parser = this;
- const { socket } = parser;
-
- if (headers === undefined) {
- headers = parser._headers;
- parser._headers = [];
- }
-
- if (url === undefined) {
- url = parser._url;
- parser._url = '';
- }
-
- // Parser is also used by http client
- const ParserIncomingMessage = (socket?.server?.[kIncomingMessage]) ||
- IncomingMessage;
-
- const incoming = parser.incoming = new ParserIncomingMessage(socket);
- incoming.httpVersionMajor = versionMajor;
- incoming.httpVersionMinor = versionMinor;
- incoming.httpVersion = versionMajor === 1 && versionMinor === 1 ?
- HTTP_VERSION_1_1 :
- `${versionMajor}.${versionMinor}`;
- incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
- parser.joinDuplicateHeaders;
- incoming.url = url;
- incoming.upgrade = upgrade;
-
- let n = headers.length;
-
- // If parser.maxHeaderPairs <= 0 assume that there's no limit.
- if (parser.maxHeaderPairs > 0)
- n = MathMin(n, parser.maxHeaderPairs);
-
- incoming._addHeaderLines(headers, n);
-
- if (typeof method === 'number') {
- // server only
- incoming.method = allMethods[method];
- } else {
- // client only
- incoming.statusCode = statusCode;
- incoming.statusMessage = statusMessage;
- }
-
- return parser.onIncoming(incoming, shouldKeepAlive);
-}
-
-function parserOnBody(b) {
- const stream = this.incoming;
-
- // If the stream has already been removed, then drop it.
- if (stream === null || stream[kSkipPendingData])
- return;
-
- // Pretend this was the result of a stream._read call.
- if (!stream._dumped) {
- const ret = stream.push(b);
- if (!ret)
- readStop(this.socket);
- }
-}
-
-function parserOnMessageComplete() {
- const parser = this;
- const stream = parser.incoming;
-
- if (stream !== null && !stream[kSkipPendingData]) {
- stream.complete = true;
- // Emit any trailing headers.
- const headers = parser._headers;
- if (headers.length) {
- stream._addHeaderLines(headers, headers.length);
- parser._headers = [];
- parser._url = '';
- }
-
- // For emit end event
- stream.push(null);
- }
-
- // Force to read the next incoming message
- readStart(parser.socket);
-}
-
-
-const parsers = new FreeList('parsers', 1000, function parsersCb() {
- const parser = new HTTPParser();
-
- cleanParser(parser);
-
- parser[kOnHeaders] = parserOnHeaders;
- parser[kOnHeadersComplete] = parserOnHeadersComplete;
- parser[kOnBody] = parserOnBody;
- parser[kOnMessageComplete] = parserOnMessageComplete;
-
- return parser;
-});
-
-function closeParserInstance(parser) { parser.close(); }
-
-// Free the parser and also break any links that it
-// might have to any other things.
-// TODO: All parser data should be attached to a
-// single object, so that it can be easily cleaned
-// up by doing `parser.data = {}`, which should
-// be done in FreeList.free. `parsers.free(parser)`
-// should be all that is needed.
-function freeParser(parser, req, socket) {
- if (parser) {
- if (parser._consumed)
- parser.unconsume();
- parser.remove();
- cleanParser(parser);
- if (parsers.free(parser) === false) {
- // Make sure the parser's stack has unwound before deleting the
- // corresponding C++ object through .close().
- setImmediate(closeParserInstance, parser);
- } else {
- // Since the Parser destructor isn't going to run the destroy() callbacks
- // it needs to be triggered manually.
- parser.free();
- }
- }
- if (req) {
- req.parser = null;
- }
- if (socket) {
- socket.parser = null;
- }
-}
-
-// Character code ranges for valid HTTP tokens
-// Valid chars: ^_`a-zA-Z-0-9!#$%&'*+.|~
-// Based on RFC 7230 Section 3.2.6 token definition
-// See https://tools.ietf.org/html/rfc7230#section-3.2.6
-const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
-const validTokenChars = new Uint8Array([
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
- 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./)
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?)
- 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O)
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_)
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o)
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~)
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 240-255
-]);
-
-/**
- * Verifies that the given val is a valid HTTP token
- * per the rules defined in RFC 7230
- * See https://tools.ietf.org/html/rfc7230#section-3.2.6
- * @param {string} val
- * @returns {boolean}
- */
-function checkIsHttpToken(val) {
- if (val.length >= 10) {
- return tokenRegExp.test(val);
- }
-
- if (val.length === 0) return false;
-
- // Use lookup table for short strings, regex for longer ones
- for (let i = 0; i < val.length; i++) {
- if (!validTokenChars[val.charCodeAt(i)]) {
- return false;
- }
- }
- return true;
-}
-
-// Strict header value regex per RFC 7230 (original/default behavior):
-// field-value = *( field-content / obs-fold )
-// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
-// field-vchar = VCHAR / obs-text
-// This rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f).
-const strictHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
-
-// Lenient header value regex per Fetch spec (https://fetch.spec.whatwg.org/#header-value):
-// - Must contain no 0x00 (NUL) or HTTP newline bytes (0x0a LF, 0x0d CR)
-// - Must be byte sequences (0x00-0xff), not arbitrary unicode
-// This allows most control characters except NUL, CR, and LF.
-// eslint-disable-next-line no-control-regex
-const lenientHeaderCharRegex = /[\x00\x0a\x0d]|[^\x00-\xff]/;
-
-/**
- * True if val contains an invalid header value character.
- * By default uses strict validation per RFC 7230.
- * When lenient=true, uses relaxed validation per Fetch spec.
- * @param {string} val
- * @param {boolean} [lenient] - Use lenient validation (Fetch spec rules)
- * @returns {boolean}
- */
-function checkInvalidHeaderChar(val, lenient = false) {
- const regex = lenient ? lenientHeaderCharRegex : strictHeaderCharRegex;
- return regex.test(val);
-}
-
-function cleanParser(parser) {
- parser._headers = [];
- parser._url = '';
- parser.socket = null;
- parser.incoming = null;
- parser.outgoing = null;
- parser.maxHeaderPairs = MAX_HEADER_PAIRS;
- parser[kOnMessageBegin] = null;
- parser[kOnExecute] = null;
- parser[kOnTimeout] = null;
- parser._consumed = false;
- parser.onIncoming = null;
- parser.joinDuplicateHeaders = null;
-}
-
-function prepareError(err, parser, rawPacket) {
- err.rawPacket = rawPacket || parser.getCurrentBuffer();
- if (typeof err.reason === 'string')
- err.message = `Parse Error: ${err.reason}`;
-}
-
-let warnedLenient = false;
-
-function isLenient() {
- if (insecureHTTPParser && !warnedLenient) {
- warnedLenient = true;
- process.emitWarning('Using insecure HTTP parsing');
- }
- return insecureHTTPParser;
-}
+ _checkInvalidHeaderChar,
+ _checkIsHttpToken,
+ chunkExpression,
+ continueExpression,
+ CRLF,
+ freeParser,
+ methods,
+ parsers,
+ kIncomingMessage,
+ HTTPParser,
+ isLenient,
+ calculateLenientFlags,
+ prepareError,
+ kSkipPendingData,
+} = require('internal/http/common');
-function calculateLenientFlags(httpValidation, insecureHTTPParserOption) {
- if (httpValidation === 'strict') {
- return HTTPParser.kLenientNone | 0;
- } else if (httpValidation === 'relaxed') {
- return HTTPParser.kLenientHeaderValueRelaxed | 0;
- } else if (httpValidation === 'insecure') {
- return HTTPParser.kLenientAll | 0;
- }
- const lenient = insecureHTTPParserOption === undefined ?
- isLenient() : insecureHTTPParserOption;
- return lenient ? HTTPParser.kLenientAll | 0 : HTTPParser.kLenientNone | 0;
-}
module.exports = {
- _checkInvalidHeaderChar: checkInvalidHeaderChar,
- _checkIsHttpToken: checkIsHttpToken,
- chunkExpression: /(?:^|\W)chunked(?:$|\W)/i,
- continueExpression: /(?:^|\W)100-continue(?:$|\W)/i,
- CRLF: '\r\n', // TODO: Deprecate this.
+ _checkInvalidHeaderChar,
+ _checkIsHttpToken,
+ chunkExpression,
+ continueExpression,
+ CRLF, // TODO: Deprecate this.
freeParser,
methods,
parsers,
@@ -343,3 +55,6 @@ module.exports = {
prepareError,
kSkipPendingData,
};
+
+process.emitWarning('The _http_common module is deprecated.',
+ 'DeprecationWarning', 'DEP0199');
diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js
index 0aa7d30580e..5a50d13460e 100644
--- a/lib/_http_incoming.js
+++ b/lib/_http_incoming.js
@@ -22,615 +22,14 @@
'use strict';
const {
- ObjectDefineProperty,
- ObjectSetPrototypeOf,
- Symbol,
-} = primordials;
-
-const { Readable, finished } = require('stream');
-const {
- deprecateInstantiation,
-} = require('internal/util');
-
-const { AbortController } = require('internal/abort_controller');
-
-const kHeaders = Symbol('kHeaders');
-const kHeadersDistinct = Symbol('kHeadersDistinct');
-const kHeadersCount = Symbol('kHeadersCount');
-const kTrailers = Symbol('kTrailers');
-const kTrailersDistinct = Symbol('kTrailersDistinct');
-const kTrailersCount = Symbol('kTrailersCount');
-const kAbortController = Symbol('kAbortController');
-const kAbortSignalSocket = Symbol('kAbortSignalSocket');
-const kAbortSignalListener = Symbol('kAbortSignalListener');
-const kAbortSignalDetached = Symbol('kAbortSignalDetached');
-const kAttachAbortSignal = Symbol('kAttachAbortSignal');
-const kDetachAbortSignal = Symbol('kDetachAbortSignal');
-
-function readStart(socket) {
- if (socket && !socket._paused && socket.readable)
- socket.resume();
-}
-
-function readStop(socket) {
- if (socket)
- socket.pause();
-}
-
-/* Abstract base class for ServerRequest and ClientResponse. */
-function IncomingMessage(socket) {
- if (!(this instanceof IncomingMessage)) {
- return deprecateInstantiation(IncomingMessage, 'DEP0195', socket);
- }
-
- Readable.call(this);
-
- if (socket) {
- const hwm = socket.readableHighWaterMark;
- if (this._readableState.highWaterMark !== hwm) {
- this._readableState.highWaterMark = hwm;
- }
- }
-
- this._readableState.readingMore = true;
-
- this.socket = socket;
-
- this.httpVersionMajor = null;
- this.httpVersionMinor = null;
- this.httpVersion = null;
- this.complete = false;
- this[kHeaders] = null;
- this[kHeadersCount] = 0;
- this.rawHeaders = [];
- this[kTrailers] = null;
- this[kTrailersCount] = 0;
- this.rawTrailers = [];
- this.joinDuplicateHeaders = false;
- this.aborted = false;
-
- this.upgrade = null;
-
- // request (server) only
- this.url = '';
- this.method = null;
-
- // response (client) only
- this.statusCode = null;
- this.statusMessage = null;
- this.client = socket;
-
- this._consuming = false;
- // Flag for when we decide that this message cannot possibly be
- // read by the user, so there's no point continuing to handle it.
- this._dumped = false;
- this[kAbortController] = null;
- this[kAbortSignalSocket] = null;
- this[kAbortSignalListener] = null;
- this[kAbortSignalDetached] = false;
-}
-ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype);
-ObjectSetPrototypeOf(IncomingMessage, Readable);
-
-ObjectDefineProperty(IncomingMessage.prototype, 'connection', {
- __proto__: null,
- get: function() {
- return this.socket;
- },
- set: function(val) {
- this.socket = val;
- },
-});
-
-ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
- __proto__: null,
- get: function() {
- if (!this[kHeaders]) {
- this[kHeaders] = { __proto__: null };
-
- const src = this.rawHeaders;
- const dst = this[kHeaders];
-
- for (let n = 0; n < this[kHeadersCount]; n += 2) {
- this._addHeaderLine(src[n + 0], src[n + 1], dst);
- }
- }
- return this[kHeaders];
- },
- set: function(val) {
- this[kHeaders] = val;
- },
-});
-
-ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
- __proto__: null,
- get: function() {
- if (!this[kHeadersDistinct]) {
- this[kHeadersDistinct] = { __proto__: null };
-
- const src = this.rawHeaders;
- const dst = this[kHeadersDistinct];
-
- for (let n = 0; n < this[kHeadersCount]; n += 2) {
- this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
- }
- }
- return this[kHeadersDistinct];
- },
- set: function(val) {
- this[kHeadersDistinct] = val;
- },
-});
-
-ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
- __proto__: null,
- get: function() {
- if (!this[kTrailers]) {
- this[kTrailers] = { __proto__: null };
-
- const src = this.rawTrailers;
- const dst = this[kTrailers];
-
- for (let n = 0; n < this[kTrailersCount]; n += 2) {
- this._addHeaderLine(src[n + 0], src[n + 1], dst);
- }
- }
- return this[kTrailers];
- },
- set: function(val) {
- this[kTrailers] = val;
- },
-});
-
-ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
- __proto__: null,
- get: function() {
- if (!this[kTrailersDistinct]) {
- this[kTrailersDistinct] = { __proto__: null };
-
- const src = this.rawTrailers;
- const dst = this[kTrailersDistinct];
-
- for (let n = 0; n < this[kTrailersCount]; n += 2) {
- this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
- }
- }
- return this[kTrailersDistinct];
- },
- set: function(val) {
- this[kTrailersDistinct] = val;
- },
-});
-
-ObjectDefineProperty(IncomingMessage.prototype, 'signal', {
- __proto__: null,
- configurable: true,
- get: function() {
- if (this[kAbortController] === null) {
- const ac = new AbortController();
- this[kAbortController] = ac;
- if (this.destroyed && (!this.readableEnded || !this.complete)) {
- ac.abort();
- } else {
- this[kAttachAbortSignal]();
- }
- }
- return this[kAbortController].signal;
- },
-});
-
-IncomingMessage.prototype[kAttachAbortSignal] = function() {
- if (this[kAbortController].signal.aborted ||
- this[kAbortSignalDetached] ||
- this[kAbortSignalListener] !== null) {
- return;
- }
-
- const socket = this.socket;
- if (!socket) {
- return;
- }
-
- if (socket.destroyed) {
- abortSignal(this);
- return;
- }
-
- this[kAbortSignalSocket] = socket;
- this[kAbortSignalListener] = () => {
- abortSignal(this);
- };
- socket.once('close', this[kAbortSignalListener]);
-};
-
-IncomingMessage.prototype[kDetachAbortSignal] = function() {
- const socket = this[kAbortSignalSocket];
- const listener = this[kAbortSignalListener];
- this[kAbortSignalDetached] = true;
- this[kAbortSignalSocket] = null;
- this[kAbortSignalListener] = null;
- if (socket !== null && listener !== null) {
- socket.removeListener('close', listener);
- }
-};
-
-IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
- if (callback)
- this.on('timeout', callback);
- this.socket.setTimeout(msecs);
- return this;
-};
-
-IncomingMessage.prototype._read = function _read() {
- if (!this._consuming) {
- this._readableState.readingMore = false;
- this._consuming = true;
- }
-
- // We actually do almost nothing here, because the parserOnBody
- // function fills up our internal buffer directly. However, we
- // do need to unpause the underlying socket so that it flows.
- if (this.socket.readable)
- readStart(this.socket);
-};
-
-// 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.
-IncomingMessage.prototype._destroy = function _destroy(err, cb) {
- if (!this.readableEnded || !this.complete) {
- this.aborted = true;
- this.emit('aborted');
- abortSignal(this);
- }
-
- // If aborted and the underlying socket is not already destroyed,
- // destroy it.
- // We have to check if the socket is already destroyed because finished
- // does not call the callback when this method is invoked from `_http_client`
- // in `test/parallel/test-http-client-spurious-aborted.js`
- if (this.socket && !this.socket.destroyed && this.aborted) {
- this.socket.destroy(err);
- const cleanup = finished(this.socket, (e) => {
- if (e?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
- e = null;
- }
- cleanup();
- process.nextTick(onError, this, e || err, cb);
- });
- } else if (err == null && !this.aborted) {
- // The message was received completely and is being destroyed cleanly:
- // complete the destroy synchronously. 'close' is still emitted on a
- // later tick by the stream machinery. The deferral below only exists
- // so that 'error' listeners attached right after destroy(err) still
- // receive the error, which cannot matter when there is no error.
- cb();
- } else {
- process.nextTick(onError, this, err, cb);
- }
-};
-
-function abortSignal(self) {
- self[kDetachAbortSignal]();
- if (self[kAbortController] !== null) {
- self[kAbortController].abort();
- }
-}
-
-IncomingMessage.prototype._addHeaderLines = _addHeaderLines;
-function _addHeaderLines(headers, n) {
- if (headers?.length) {
- let dest;
- if (this.complete) {
- this.rawTrailers = headers;
- this[kTrailersCount] = n;
- dest = this[kTrailers];
- } else {
- this.rawHeaders = headers;
- this[kHeadersCount] = n;
- dest = this[kHeaders];
- }
-
- if (dest) {
- for (let i = 0; i < n; i += 2) {
- this._addHeaderLine(headers[i], headers[i + 1], dest);
- }
- }
- }
-}
-
-
-// How repeated instances of a header field are combined in the headers
-// object. Fields that are not known are joined with ', '.
-const kFirstWins = 0;
-const kJoinComma = 1;
-const kJoinSemicolon = 2;
-const kArray = 3;
-
-function knownField(name, merge) {
- return { name, merge };
-}
-
-// Later values are dropped, unless joinDuplicateHeaders is set.
-const kAge = knownField('age', kFirstWins);
-const kHost = knownField('host', kFirstWins);
-const kFrom = knownField('from', kFirstWins);
-const kETag = knownField('etag', kFirstWins);
-const kServer = knownField('server', kFirstWins);
-const kReferer = knownField('referer', kFirstWins);
-const kExpires = knownField('expires', kFirstWins);
-const kLocation = knownField('location', kFirstWins);
-const kUserAgent = knownField('user-agent', kFirstWins);
-const kRetryAfter = knownField('retry-after', kFirstWins);
-const kContentType = knownField('content-type', kFirstWins);
-const kMaxForwards = knownField('max-forwards', kFirstWins);
-const kAuthorization = knownField('authorization', kFirstWins);
-const kLastModified = knownField('last-modified', kFirstWins);
-const kContentLength = knownField('content-length', kFirstWins);
-const kIfModifiedSince = knownField('if-modified-since', kFirstWins);
-const kProxyAuthorization = knownField('proxy-authorization', kFirstWins);
-const kIfUnmodifiedSince = knownField('if-unmodified-since', kFirstWins);
-
-// Values are joined with ', '.
-const kDate = knownField('date', kJoinComma);
-const kVary = knownField('vary', kJoinComma);
-const kOrigin = knownField('origin', kJoinComma);
-const kExpect = knownField('expect', kJoinComma);
-const kAccept = knownField('accept', kJoinComma);
-const kUpgrade = knownField('upgrade', kJoinComma);
-const kIfMatch = knownField('if-match', kJoinComma);
-const kConnection = knownField('connection', kJoinComma);
-const kCacheControl = knownField('cache-control', kJoinComma);
-const kIfNoneMatch = knownField('if-none-match', kJoinComma);
-const kAcceptEncoding = knownField('accept-encoding', kJoinComma);
-const kAcceptLanguage = knownField('accept-language', kJoinComma);
-const kXForwardedFor = knownField('x-forwarded-for', kJoinComma);
-const kContentEncoding = knownField('content-encoding', kJoinComma);
-const kXForwardedHost = knownField('x-forwarded-host', kJoinComma);
-const kTransferEncoding = knownField('transfer-encoding', kJoinComma);
-const kXForwardedProto = knownField('x-forwarded-proto', kJoinComma);
-
-// Values are joined with '; '.
-const kCookie = knownField('cookie', kJoinSemicolon);
-
-// Values are collected into an array.
-const kSetCookie = knownField('set-cookie', kArray);
-
-// Returns the descriptor of a known field, or the lowercased name of any other
-// field. The 'traditional cased' and lowercase spellings of known fields are
-// matched first to avoid calling toLowerCase() for them.
-// TODO: perhaps http_parser could be returning both raw and lowercased versions
-// of known header names to avoid us having to call toLowerCase() for those
-// headers.
-function matchKnownFields(field, lowercased) {
- switch (field.length) {
- case 3:
- if (field === 'Age' || field === 'age') return kAge;
- break;
- case 4:
- if (field === 'Host' || field === 'host') return kHost;
- if (field === 'From' || field === 'from') return kFrom;
- if (field === 'ETag' || field === 'etag') return kETag;
- if (field === 'Date' || field === 'date') return kDate;
- if (field === 'Vary' || field === 'vary') return kVary;
- break;
- case 6:
- if (field === 'Server' || field === 'server') return kServer;
- if (field === 'Cookie' || field === 'cookie') return kCookie;
- if (field === 'Origin' || field === 'origin') return kOrigin;
- if (field === 'Expect' || field === 'expect') return kExpect;
- if (field === 'Accept' || field === 'accept') return kAccept;
- break;
- case 7:
- if (field === 'Referer' || field === 'referer') return kReferer;
- if (field === 'Expires' || field === 'expires') return kExpires;
- if (field === 'Upgrade' || field === 'upgrade') return kUpgrade;
- break;
- case 8:
- if (field === 'Location' || field === 'location')
- return kLocation;
- if (field === 'If-Match' || field === 'if-match')
- return kIfMatch;
- break;
- case 10:
- if (field === 'User-Agent' || field === 'user-agent')
- return kUserAgent;
- if (field === 'Set-Cookie' || field === 'set-cookie')
- return kSetCookie;
- if (field === 'Connection' || field === 'connection')
- return kConnection;
- break;
- case 11:
- if (field === 'Retry-After' || field === 'retry-after')
- return kRetryAfter;
- break;
- case 12:
- if (field === 'Content-Type' || field === 'content-type')
- return kContentType;
- if (field === 'Max-Forwards' || field === 'max-forwards')
- return kMaxForwards;
- break;
- case 13:
- if (field === 'Authorization' || field === 'authorization')
- return kAuthorization;
- if (field === 'Last-Modified' || field === 'last-modified')
- return kLastModified;
- if (field === 'Cache-Control' || field === 'cache-control')
- return kCacheControl;
- if (field === 'If-None-Match' || field === 'if-none-match')
- return kIfNoneMatch;
- break;
- case 14:
- if (field === 'Content-Length' || field === 'content-length')
- return kContentLength;
- break;
- case 15:
- if (field === 'Accept-Encoding' || field === 'accept-encoding')
- return kAcceptEncoding;
- if (field === 'Accept-Language' || field === 'accept-language')
- return kAcceptLanguage;
- if (field === 'X-Forwarded-For' || field === 'x-forwarded-for')
- return kXForwardedFor;
- break;
- case 16:
- if (field === 'Content-Encoding' || field === 'content-encoding')
- return kContentEncoding;
- if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host')
- return kXForwardedHost;
- break;
- case 17:
- if (field === 'If-Modified-Since' || field === 'if-modified-since')
- return kIfModifiedSince;
- if (field === 'Transfer-Encoding' || field === 'transfer-encoding')
- return kTransferEncoding;
- if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto')
- return kXForwardedProto;
- break;
- case 19:
- if (field === 'Proxy-Authorization' || field === 'proxy-authorization')
- return kProxyAuthorization;
- if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since')
- return kIfUnmodifiedSince;
- break;
- }
- if (lowercased) {
- return field;
- }
- return matchKnownFields(field.toLowerCase(), true);
-}
-// Add the given (field, value) pair to the message
-//
-// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
-// same header with a ', ' if the header in question supports specification of
-// multiple values this way. The one exception to this is the Cookie header,
-// which has multiple values joined with a '; ' instead. If a header's values
-// cannot be joined in either of these ways, we declare the first instance the
-// winner and drop the second. Fields that are not known are always joined.
-IncomingMessage.prototype._addHeaderLine = _addHeaderLine;
-function _addHeaderLine(field, value, dest) {
- const known = matchKnownFields(field);
- let merge = kJoinComma;
- if (typeof known === 'string') {
- field = known;
- } else {
- field = known.name;
- merge = known.merge;
- }
- if (merge === kJoinComma || merge === kJoinSemicolon) {
- // Make a delimited list
- if (typeof dest[field] === 'string') {
- dest[field] += (merge === kJoinComma ? ', ' : '; ') + value;
- } else {
- dest[field] = value;
- }
- } else if (merge === kArray) {
- // Array header -- only Set-Cookie at the moment
- if (dest['set-cookie'] !== undefined) {
- dest['set-cookie'].push(value);
- } else {
- dest['set-cookie'] = [value];
- }
- } else if (this.joinDuplicateHeaders) {
- // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
- // https://github.com/nodejs/node/issues/45699
- // allow authorization multiple fields
- // Make a delimited list
- if (dest[field] === undefined) {
- dest[field] = value;
- } else {
- dest[field] += ', ' + value;
- }
- } else if (dest[field] === undefined) {
- // Drop duplicates
- dest[field] = value;
- }
-}
-
-IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct;
-function _addHeaderLineDistinct(field, value, dest) {
- field = field.toLowerCase();
- if (!dest[field]) {
- dest[field] = [value];
- } else {
- dest[field].push(value);
- }
-}
-
-IncomingMessage.prototype._dumpAndCloseReadable = function _dumpAndCloseReadable() {
- this._dumped = true;
- this._readableState.ended = true;
- this._readableState.endEmitted = true;
- this._readableState.destroyed = true;
- this._readableState.closed = true;
- this._readableState.closeEmitted = true;
-};
-
-
-// Call this instead of resume() if we want to just
-// dump all the data to /dev/null
-IncomingMessage.prototype._dump = function _dump() {
- if (!this._dumped) {
- this._dumped = true;
- // If there is buffered data, it may trigger 'data' events.
- // Remove 'data' event listeners explicitly.
- this.removeAllListeners('data');
- if (this.complete && !this.destroyed && this.readableLength === 0) {
- // The message was fully received and never read: there is nothing to
- // pull off the wire. Go straight to the 'end' emission instead of
- // paying for the resume() and flow() scheduling machinery.
- this.read(0);
- } else {
- this.resume();
- }
- }
-};
-
-// Case-insensitive ASCII compare against an already-lowercased name.
-// Avoids allocating a lowercased copy of every header name.
-function asciiEqualIgnoreCase(a, lower) {
- const len = lower.length;
- if (a.length !== len)
- return false;
- if (a === lower)
- return true;
- for (let i = 0; i < len; i++) {
- let c = a.charCodeAt(i);
- if (c >= 65 && c <= 90)
- c += 32;
- if (c !== lower.charCodeAt(i))
- return false;
- }
- return true;
-}
-
-function getRawHeader(msg, lowerName, joinDuplicates) {
- const rawHeaders = msg.rawHeaders;
- const count = msg[kHeadersCount];
- let result;
- for (let i = 0; i < count; i += 2) {
- if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
- continue;
- const value = rawHeaders[i + 1];
- if (result === undefined) {
- result = value;
- if (!joinDuplicates)
- return result;
- } else {
- result += ', ' + value;
- }
- }
- return result;
-}
-
-function onError(self, error, cb) {
- // This is to keep backward compatible behavior.
- // An error is emitted only if there are listeners attached to the event.
- if (self.listenerCount('error') === 0) {
- cb();
- } else {
- cb(error);
- }
-}
+ IncomingMessage,
+ kDetachAbortSignal,
+ kHeadersCount,
+ asciiEqualIgnoreCase,
+ getRawHeader,
+ readStart,
+ readStop,
+} = require('internal/http/incoming');
module.exports = {
IncomingMessage,
@@ -641,3 +40,6 @@ module.exports = {
readStart,
readStop,
};
+
+process.emitWarning('The _http_incoming module is deprecated. Use `node:http` instead.',
+ 'DeprecationWarning', 'DEP0199');
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
index f6ce0e5a475..360efe39946 100644
--- a/lib/_http_outgoing.js
+++ b/lib/_http_outgoing.js
@@ -22,1460 +22,15 @@
'use strict';
const {
- 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, utcDateHeader, 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,
- ERR_UNKNOWN_ENCODING,
- },
- hideStackFrames,
-} = require('internal/errors');
-const {
- validateObject,
- validateOneOf,
- 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');
-
-// Container for the headers set through `setHeader()`, keyed by the
-// lower-cased header name. It has no enumerable or inherited properties, like
-// a `{ __proto__: null }` literal, but instances are fast-mode objects: V8
-// creates null-prototype literals in dictionary mode, which costs a hash table
-// allocation per response and slower property access for every header.
-class OutgoingHeaders {}
-ObjectSetPrototypeOf(OutgoingHeaders.prototype, null);
-delete OutgoingHeaders.prototype.constructor;
-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 kLenientCache = Symbol('kLenientCache');
-
-let keepAliveTimeoutCache = -1;
-let keepAliveMaxCache = -1;
-let keepAliveHeaderCache = '';
-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);
- }
-
- // Preset the listener table with the events that are commonly listened
- // for, mirroring streams. The presence of the properties keeps `_events`
- // in fast mode: the first `on()` call would otherwise have to allocate a
- // dictionary for the `{ __proto__: null }` object created by EventEmitter.
- this._events ??= {
- close: undefined,
- error: undefined,
- finish: undefined,
- prefinish: undefined,
- drain: undefined,
- };
-
- 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;
- this[kLenientCache] = null;
-}
-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() {
- // 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 (msg.httpValidation !== undefined) {
- return msg.httpValidation !== 'strict';
- }
- // ServerResponse: check server's httpValidation option
- const serverHttpValidation = msg.req?.socket?.server?.httpValidation;
- if (serverHttpValidation !== undefined) {
- return serverHttpValidation !== 'strict';
- }
- // Legacy insecureHTTPParser - ClientRequest has it directly
- if (typeof msg.insecureHTTPParser === 'boolean') {
- return msg.insecureHTTPParser;
- }
- // ServerResponse can access via req.socket.server
- 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,
- 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() {
- let bufferedLength = this[kChunkedLength];
- if (bufferedLength !== 0) {
- bufferedLength += bufferedLength.toString(16).length + 4 +
- (!this._headerSent && this._header !== null ? this._header.length : 0);
- }
- return this.outputSize + bufferedLength + (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]--;
- const socket = this[kSocket];
- try {
- if (this[kCorked] || this[kChunkedBuffer].length === 0) {
- return;
- }
-
- const len = this[kChunkedLength];
- const buf = this[kChunkedBuffer];
- if (this.destroyed || socket?.destroyed) {
- const error = this[kErrored] || socket?._writableState?.errored || new ERR_STREAM_DESTROYED('write');
- for (let n = 2; n < buf.length; n += 3) {
- if (buf[n] !== nop) {
- process.nextTick(buf[n], error);
- }
- }
- buf.length = 0;
- this[kChunkedLength] = 0;
- return;
- }
- assert(this.chunkedEncoding);
-
- const 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);
- callbacks.push(buf[n + 2]);
- }
- this._send(crlf_buf, null, (err) => {
- for (const callback of callbacks) {
- callback(err);
- }
- });
-
- this[kChunkedBuffer].length = 0;
- this[kChunkedLength] = 0;
- } finally {
- socket?.uncork();
- }
-
- // 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[kChunkedBuffer].length !== 0) {
- this[kCorked] = 1;
- this.uncork();
- }
-
- 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 += utcDateHeader();
- }
-
- // 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) {
- if (this._keepAliveTimeout && this._defaultKeepAlive) {
- // 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 += keepAliveHeaderCache;
- } else {
- header += 'Connection: keep-alive\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 lowerOutgoingHeaderName(field) {
- switch (field) {
- case 'Connection':
- case 'connection':
- return 'connection';
- case 'Content-Length':
- case 'content-length':
- return 'content-length';
- case 'Transfer-Encoding':
- case 'transfer-encoding':
- return 'transfer-encoding';
- case 'Date':
- case 'date':
- return 'date';
- case 'Expect':
- case 'expect':
- return 'expect';
- case 'Trailer':
- case 'trailer':
- return 'trailer';
- case 'Keep-Alive':
- case 'keep-alive':
- return 'keep-alive';
- default:
- return field.toLowerCase();
- }
-}
-
-function matchHeader(self, state, field, value) {
- if (field.length < 4 || field.length > 17)
- return;
- field = lowerOutgoingHeaderName(field);
- 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 (!isValidHeaderName(name)) {
- throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name);
- }
-}));
-
-/**
- * Non-throwing counterpart of `validateHeaderName()`.
- * @param {any} name
- * @returns {boolean}
- */
-function isValidHeaderName(name) {
- return typeof name === 'string' && checkIsHttpToken(name);
-}
-
-const kHttpValidationValues = ['strict', 'relaxed'];
-
-/**
- * Non-throwing counterpart of `validateHeaderValue()`.
- * @param {any} value
- * @param {{ httpValidation?: 'strict' | 'relaxed' }} [options]
- * @returns {boolean}
- */
-function isValidHeaderValue(value, options) {
- let lenient = false;
- if (options !== undefined) {
- validateObject(options, 'options');
- const { httpValidation } = options;
- if (httpValidation === 'relaxed') {
- lenient = true;
- } else if (httpValidation !== undefined && httpValidation !== 'strict') {
- validateOneOf(httpValidation, 'options.httpValidation', kHttpValidationValues);
- }
- }
- if (value === undefined || typeof value === 'symbol') {
- return false;
- }
- return !checkInvalidHeaderChar(value, lenient);
-}
-
-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 = new OutgoingHeaders();
-
- 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 headers = ObjectValues(headersMap);
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0; i < headers.length; i++) {
- headers[i] = headers[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 (msg.chunkedEncoding && typeof chunk === 'string' && encoding && !Buffer.isEncoding(encoding)) {
- throw new ERR_UNKNOWN_ENCODING(encoding);
- }
-
- if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
- msg.cork();
- process.nextTick(connectionCorkNT, msg);
- }
-
- let ret;
- if (msg.chunkedEncoding && chunk.length !== 0) {
- len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
- if (msg[kCorked]) {
- if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) {
- chunk = Stream._uint8ArrayToBuffer(chunk);
- }
- msg[kChunkedBuffer].push(chunk, encoding, callback);
- msg[kChunkedLength] += len;
- ret = msg.writableLength < msg.writableHighWaterMark;
- } 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 if (msg.chunkedEncoding && msg[kCorked] && msg[kChunkedBuffer].length !== 0) {
- msg[kChunkedBuffer].push(chunk, encoding, callback);
- ret = msg.writableLength < msg.writableHighWaterMark;
- } else {
- ret = msg._send(chunk, encoding, callback, len);
- }
-
- debug('write ret = ' + ret);
- return ret;
-}
-
-
-// If this last write can be delivered immediately as the final chunk, this
-// prepares to do so, and then returns true. If not, it returns false and
-// a separate _send call and tick will be required to finish up.
-function maybePrepareFinalChunk(msg, chunk, encoding) {
- if (typeof chunk !== 'string' && !isUint8Array(chunk))
- return false;
-
- if (msg.destroyed || msg.strictContentLength)
- return false;
-
- if (!msg._header) {
- msg._contentLength = typeof chunk === 'string' ?
- Buffer.byteLength(chunk, encoding) : chunk.byteLength;
- msg._implicitHeader();
- }
-
- return !!msg._header && msg._hasBody && !msg.chunkedEncoding;
-}
-
-function connectionCorkNT(msg) {
- if (msg[kCorked]) {
- msg.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;
- }
-
- let finishCallback = 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();
- }
-
- if (maybePrepareFinalChunk(this, chunk, encoding)) {
- // If just one final write is required, with nothing to follow, we
- // attach finish to the write to avoid a separate send() & tick step
- // later on - this is purely a performance optimization.
- if (typeof callback === 'function') {
- queueEndCallback(this, callback);
- callback = undefined;
- }
- finishCallback = onFinish.bind(undefined, this);
- }
-
- write_(this, chunk, encoding, finishCallback, 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);
- }
-
- // Flush buffered chunks before terminating the response.
- if (this[kChunkedBuffer].length !== 0) {
- this[kSocket]?.cork();
- this[kCorked] = 1;
- this.uncork();
- }
-
- if (finishCallback === null) {
- // If we didn't early finish, send the last data and schedule 'finish' now:
- finishCallback = onFinish.bind(undefined, this);
-
- if (this._hasBody && this.chunkedEncoding) {
- this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finishCallback);
- } else if (!this._headerSent || this.writableLength || chunk) {
- this._send('', 'latin1', finishCallback);
- } else {
- process.nextTick(finishCallback);
- }
- }
-
- 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);
-};
+ kHighWaterMark,
+ kUniqueHeaders,
+ parseUniqueHeadersOption,
+ isValidHeaderName,
+ isValidHeaderValue,
+ validateHeaderName,
+ validateHeaderValue,
+ OutgoingMessage,
+} = require('internal/http/outgoing');
module.exports = {
kHighWaterMark,
@@ -1487,3 +42,6 @@ module.exports = {
validateHeaderValue,
OutgoingMessage,
};
+
+process.emitWarning('The _http_outgoing module is deprecated. Use `node:http` instead.',
+ 'DeprecationWarning', 'DEP0199');
diff --git a/lib/_http_server.js b/lib/_http_server.js
index 43af4fff0a4..830250b5302 100644
--- a/lib/_http_server.js
+++ b/lib/_http_server.js
@@ -22,1519 +22,17 @@
'use strict';
const {
- ArrayIsArray,
- Error,
- MathMin,
- NumberIsFinite,
- ObjectKeys,
- ObjectSetPrototypeOf,
- ReflectApply,
- Symbol,
- SymbolAsyncDispose,
- SymbolFor,
-} = primordials;
-
-const { Duplex } = require('stream');
-const net = require('net');
-const EE = require('events');
-const assert = require('internal/assert');
-const {
- parsers,
- freeParser,
- continueExpression,
- chunkExpression,
- kIncomingMessage,
- kSocket,
- HTTPParser,
- calculateLenientFlags,
- _checkInvalidHeaderChar: checkInvalidHeaderChar,
- prepareError,
-} = require('_http_common');
-const { ConnectionsList } = internalBinding('http_parser');
-const {
- kUniqueHeaders,
- parseUniqueHeadersOption,
- OutgoingMessage,
- validateHeaderName,
- validateHeaderValue,
-} = require('_http_outgoing');
-const {
- kOutHeaders,
- kNeedDrain,
- isTraceHTTPEnabled,
- traceBegin,
- traceEnd,
- getNextTraceEventId,
-} = require('internal/http');
-const {
- defaultTriggerAsyncIdScope,
- getOrSetAsyncId,
-} = require('internal/async_hooks');
-const {
- IncomingMessage,
- kDetachAbortSignal,
- kHeadersCount,
- asciiEqualIgnoreCase,
- getRawHeader,
-} = require('_http_incoming');
-const {
- ConnResetException,
- codes: {
- ERR_HTTP_HEADERS_SENT,
- ERR_HTTP_INVALID_STATUS_CODE,
- ERR_HTTP_REQUEST_TIMEOUT,
- ERR_HTTP_SOCKET_ASSIGNED,
- ERR_HTTP_SOCKET_ENCODING,
- ERR_INVALID_ARG_VALUE,
- ERR_INVALID_CHAR,
- ERR_OUT_OF_RANGE,
- },
-} = require('internal/errors');
-const {
- assignFunctionName,
- deprecateInstantiation,
- kEmptyObject,
- promisify,
-} = require('internal/util');
-const {
- validateInteger,
- validateBoolean,
- validateOneOf,
- validateLinkHeaderValue,
- validateObject,
- validateFunction,
-} = require('internal/validators');
-const Buffer = require('buffer').Buffer;
-const { setInterval, clearInterval } = require('timers');
-let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
- debug = fn;
-});
-
-const dc = require('diagnostics_channel');
-const onRequestStartChannel = dc.channel('http.server.request.start');
-const onResponseCreatedChannel = dc.channel('http.server.response.created');
-const onResponseFinishChannel = dc.channel('http.server.response.finish');
-
-const kServerResponse = Symbol('ServerResponse');
-const kServerResponseStatistics = Symbol('ServerResponseStatistics');
-const kUpgradeStream = Symbol('UpgradeStream');
-
-const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
-const kConnectionState = Symbol('ConnectionState');
-const kResponseOptions = Symbol('ResponseOptions');
-
-const statusLineCache = [];
-
-const {
- hasObserver,
- startPerf,
- stopPerf,
-} = require('internal/perf/observe');
-
-const STATUS_CODES = {
- 100: 'Continue', // RFC 7231 6.2.1
- 101: 'Switching Protocols', // RFC 7231 6.2.2
- 102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918)
- 103: 'Early Hints', // RFC 8297 2
- 200: 'OK', // RFC 7231 6.3.1
- 201: 'Created', // RFC 7231 6.3.2
- 202: 'Accepted', // RFC 7231 6.3.3
- 203: 'Non-Authoritative Information', // RFC 7231 6.3.4
- 204: 'No Content', // RFC 7231 6.3.5
- 205: 'Reset Content', // RFC 7231 6.3.6
- 206: 'Partial Content', // RFC 7233 4.1
- 207: 'Multi-Status', // RFC 4918 11.1
- 208: 'Already Reported', // RFC 5842 7.1
- 226: 'IM Used', // RFC 3229 10.4.1
- 300: 'Multiple Choices', // RFC 7231 6.4.1
- 301: 'Moved Permanently', // RFC 7231 6.4.2
- 302: 'Found', // RFC 7231 6.4.3
- 303: 'See Other', // RFC 7231 6.4.4
- 304: 'Not Modified', // RFC 7232 4.1
- 305: 'Use Proxy', // RFC 7231 6.4.5
- 307: 'Temporary Redirect', // RFC 7231 6.4.7
- 308: 'Permanent Redirect', // RFC 7238 3
- 400: 'Bad Request', // RFC 7231 6.5.1
- 401: 'Unauthorized', // RFC 7235 3.1
- 402: 'Payment Required', // RFC 7231 6.5.2
- 403: 'Forbidden', // RFC 7231 6.5.3
- 404: 'Not Found', // RFC 7231 6.5.4
- 405: 'Method Not Allowed', // RFC 7231 6.5.5
- 406: 'Not Acceptable', // RFC 7231 6.5.6
- 407: 'Proxy Authentication Required', // RFC 7235 3.2
- 408: 'Request Timeout', // RFC 7231 6.5.7
- 409: 'Conflict', // RFC 7231 6.5.8
- 410: 'Gone', // RFC 7231 6.5.9
- 411: 'Length Required', // RFC 7231 6.5.10
- 412: 'Precondition Failed', // RFC 7232 4.2
- 413: 'Payload Too Large', // RFC 7231 6.5.11
- 414: 'URI Too Long', // RFC 7231 6.5.12
- 415: 'Unsupported Media Type', // RFC 7231 6.5.13
- 416: 'Range Not Satisfiable', // RFC 7233 4.4
- 417: 'Expectation Failed', // RFC 7231 6.5.14
- 418: 'I\'m a Teapot', // RFC 7168 2.3.3
- 421: 'Misdirected Request', // RFC 7540 9.1.2
- 422: 'Unprocessable Entity', // RFC 4918 11.2
- 423: 'Locked', // RFC 4918 11.3
- 424: 'Failed Dependency', // RFC 4918 11.4
- 425: 'Too Early', // RFC 8470 5.2
- 426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15
- 428: 'Precondition Required', // RFC 6585 3
- 429: 'Too Many Requests', // RFC 6585 4
- 431: 'Request Header Fields Too Large', // RFC 6585 5
- 451: 'Unavailable For Legal Reasons', // RFC 7725 3
- 500: 'Internal Server Error', // RFC 7231 6.6.1
- 501: 'Not Implemented', // RFC 7231 6.6.2
- 502: 'Bad Gateway', // RFC 7231 6.6.3
- 503: 'Service Unavailable', // RFC 7231 6.6.4
- 504: 'Gateway Timeout', // RFC 7231 6.6.5
- 505: 'HTTP Version Not Supported', // RFC 7231 6.6.6
- 506: 'Variant Also Negotiates', // RFC 2295 8.1
- 507: 'Insufficient Storage', // RFC 4918 11.5
- 508: 'Loop Detected', // RFC 5842 7.2
- 509: 'Bandwidth Limit Exceeded',
- 510: 'Not Extended', // RFC 2774 7
- 511: 'Network Authentication Required', // RFC 6585 6
-};
-
-const kOnExecute = HTTPParser.kOnExecute | 0;
-const kOnTimeout = HTTPParser.kOnTimeout | 0;
-
-const kConnections = Symbol('http.server.connections');
-const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval');
-
-const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request';
-
-class HTTPServerAsyncResource {
- constructor(type, socket) {
- this.type = type;
- this.socket = socket;
- }
-}
-
-function ServerResponse(req, options) {
- if (!(this instanceof ServerResponse)) {
- return deprecateInstantiation(ServerResponse, 'DEP0195', req, options);
- }
-
- OutgoingMessage.call(this, options);
-
- if (req.method === 'HEAD') this._hasBody = false;
-
- this.req = req;
- this.sendDate = true;
- this._sent100 = false;
- this._expect_continue = false;
-
- if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
- this.useChunkedEncodingByDefault =
- chunkExpression.test(getRawHeader(req, 'te', true));
- this.shouldKeepAlive = false;
- }
-
- if (hasObserver('http')) {
- startPerf(this, kServerResponseStatistics, {
- type: 'http',
- name: 'HttpRequest',
- detail: {
- req: {
- method: req.method,
- url: req.url,
- headers: req.headers,
- },
- },
- });
- }
- if (isTraceHTTPEnabled()) {
- this._traceEventId = getNextTraceEventId();
- traceBegin(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId);
- }
- if (onResponseCreatedChannel.hasSubscribers) {
- onResponseCreatedChannel.publish({
- request: req,
- response: this,
- });
- }
-}
-ObjectSetPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype);
-ObjectSetPrototypeOf(ServerResponse, OutgoingMessage);
-
-ServerResponse.prototype._finish = function _finish() {
- if (this[kServerResponseStatistics] && hasObserver('http')) {
- stopPerf(this, kServerResponseStatistics, {
- detail: {
- res: {
- statusCode: this.statusCode,
- statusMessage: this.statusMessage,
- headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {},
- },
- },
- });
- }
- OutgoingMessage.prototype._finish.call(this);
- if (isTraceHTTPEnabled() && typeof this._traceEventId === 'number') {
- const data = {
- url: this.req?.url,
- statusCode: this.statusCode,
- };
- traceEnd(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId, data);
- }
-};
-
-
-ServerResponse.prototype.statusCode = 200;
-ServerResponse.prototype.statusMessage = undefined;
-
-function onServerResponseClose() {
- // EventEmitter.emit makes a copy of the 'close' listeners array before
- // calling the listeners. detachSocket() unregisters onServerResponseClose
- // but if detachSocket() is called, directly or indirectly, by a 'close'
- // listener, onServerResponseClose is still in that copy of the listeners
- // array. That is, in the example below, b still gets called even though
- // it's been removed by a:
- //
- // const EventEmitter = require('events');
- // const obj = new EventEmitter();
- // obj.on('event', a);
- // obj.on('event', b);
- // function a() { obj.removeListener('event', b) }
- // function b() { throw "BAM!" }
- // obj.emit('event'); // throws
- //
- // Ergo, we need to deal with stale 'close' events and handle the case
- // where the ServerResponse object has already been deconstructed.
- // Fortunately, that requires only a single if check. :-)
- if (this._httpMessage) {
- emitCloseNT(this._httpMessage);
- }
-}
-
-ServerResponse.prototype.assignSocket = function assignSocket(socket) {
- if (socket._httpMessage) {
- throw new ERR_HTTP_SOCKET_ASSIGNED();
- }
- socket._httpMessage = this;
- socket.on('close', onServerResponseClose);
- this.socket = socket;
- this.emit('socket', socket);
- this._flush();
-};
-
-ServerResponse.prototype.detachSocket = function detachSocket(socket) {
- assert(socket._httpMessage === this);
- socket.removeListener('close', onServerResponseClose);
- socket._httpMessage = null;
- this.socket = null;
-};
-
-ServerResponse.prototype.writeInformation = function writeInformation(
- statusCode, headers, cb) {
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT('write');
- }
-
- validateInteger(statusCode, 'statusCode', 100, 199);
- if (statusCode === 101) {
- throw new ERR_HTTP_INVALID_STATUS_CODE(statusCode);
- }
-
- const statusMessage = STATUS_CODES[statusCode] || 'unknown';
- let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`;
-
- const lenient = this._isLenientHeaderValidation();
- if (headers !== undefined && headers !== null) {
- if (ArrayIsArray(headers)) {
- if (headers.length && ArrayIsArray(headers[0])) {
- for (let i = 0; i < headers.length; i++) {
- const entry = headers[i];
- head += processInformationHeader(entry[0], entry[1], lenient);
- }
- } else {
- if (headers.length % 2 !== 0) {
- throw new ERR_INVALID_ARG_VALUE('headers', headers);
- }
- for (let i = 0; i < headers.length; i += 2) {
- head += processInformationHeader(headers[i], headers[i + 1], lenient);
- }
- }
- } else {
- validateObject(headers, 'headers');
- const keys = ObjectKeys(headers);
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- head += processInformationHeader(key, headers[key], lenient);
- }
- }
- }
-
- head += '\r\n';
-
- return this._writeRaw(head, 'ascii', cb);
-};
-
-function processInformationHeader(name, value, lenient) {
- validateHeaderName(name);
- validateHeaderValue(name, value, lenient);
- return `${name}: ${value}\r\n`;
-}
-
-ServerResponse.prototype.writeContinue = function writeContinue(cb) {
- this.writeInformation(100, null, cb);
- this._sent100 = true;
-};
-
-ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
- this.writeInformation(102, null, cb);
-};
-
-ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
- validateObject(hints, 'hints');
-
- if (hints.link === null || hints.link === undefined) {
- return;
- }
-
- const link = validateLinkHeaderValue(hints.link);
-
- if (link.length === 0) {
- return;
- }
-
- if (checkInvalidHeaderChar(link)) {
- throw new ERR_INVALID_CHAR('header content', 'Link');
- }
-
- const headers = { __proto__: null, Link: link };
- const keys = ObjectKeys(hints);
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- if (key !== 'link') {
- headers[key] = hints[key];
- }
- }
-
- this.writeInformation(103, headers, cb);
-};
-
-ServerResponse.prototype._implicitHeader = function _implicitHeader() {
- this.writeHead(this.statusCode);
-};
-
-ServerResponse.prototype.writeHead = writeHead;
-function writeHead(statusCode, reason, obj) {
-
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT('write');
- }
-
- const originalStatusCode = statusCode;
-
- statusCode |= 0;
- if (statusCode < 100 || statusCode > 999) {
- throw new ERR_HTTP_INVALID_STATUS_CODE(originalStatusCode);
- }
-
-
- if (typeof reason === 'string') {
- // writeHead(statusCode, reasonPhrase[, headers])
- this.statusMessage = reason;
- } else {
- // writeHead(statusCode[, headers])
- this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown';
- obj ??= reason;
- }
- this.statusCode = statusCode;
-
- let headers;
- if (this[kOutHeaders]) {
- // Slow-case: when progressive API and header fields are passed.
- let k;
- if (ArrayIsArray(obj)) {
- if (obj.length % 2 !== 0) {
- throw new ERR_INVALID_ARG_VALUE('headers', obj);
- }
-
- // Headers in obj should override previous headers but still
- // allow explicit duplicates. To do so, we first remove any
- // existing conflicts, then use appendHeader.
-
- for (let n = 0; n < obj.length; n += 2) {
- k = obj[n + 0];
- this.removeHeader(k);
- }
-
- for (let n = 0; n < obj.length; n += 2) {
- k = obj[n + 0];
- if (k) this.appendHeader(k, obj[n + 1]);
- }
- } else if (obj) {
- const keys = ObjectKeys(obj);
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0; i < keys.length; i++) {
- k = keys[i];
- if (k) this.setHeader(k, obj[k]);
- }
- }
- // Only progressive api is used
- headers = this[kOutHeaders];
- } else {
- // Only writeHead() called
- headers = obj;
- }
-
- 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');
-
- statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
- }
-
- if (statusCode === 204 || statusCode === 304 ||
- (statusCode >= 100 && statusCode <= 199)) {
- // RFC 2616, 10.2.5:
- // The 204 response MUST NOT include a message-body, and thus is always
- // terminated by the first empty line after the header fields.
- // RFC 2616, 10.3.5:
- // The 304 response MUST NOT contain a message-body, and thus is always
- // terminated by the first empty line after the header fields.
- // RFC 2616, 10.1 Informational 1xx:
- // This class of status code indicates a provisional response,
- // consisting only of the Status-Line and optional headers, and is
- // terminated by an empty line.
- this._hasBody = false;
- }
-
- // Don't keep alive connections where the client expects 100 Continue
- // but we sent a final status; they may put extra bytes on the wire.
- if (this._expect_continue && !this._sent100) {
- this.shouldKeepAlive = false;
- }
-
- this._storeHeader(statusLine, headers);
-
- return this;
-}
-
-function storeHTTPOptions(options) {
- this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
- this[kServerResponse] = options.ServerResponse || ServerResponse;
-
- const maxHeaderSize = options.maxHeaderSize;
- if (maxHeaderSize !== undefined)
- validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
- this.maxHeaderSize = maxHeaderSize;
-
- const optimizeEmptyRequests = options.optimizeEmptyRequests;
- if (optimizeEmptyRequests !== undefined)
- validateBoolean(optimizeEmptyRequests, 'options.optimizeEmptyRequests');
- this[kOptimizeEmptyRequests] = optimizeEmptyRequests || false;
-
- const insecureHTTPParser = options.insecureHTTPParser;
- if (insecureHTTPParser !== undefined)
- validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
- this.insecureHTTPParser = insecureHTTPParser;
-
- const httpValidation = options.httpValidation;
- if (httpValidation !== undefined) {
- validateOneOf(httpValidation, 'options.httpValidation',
- ['strict', 'relaxed', 'insecure']);
- if (insecureHTTPParser !== undefined) {
- throw new ERR_INVALID_ARG_VALUE(
- 'options.httpValidation',
- httpValidation,
- 'cannot be used together with options.insecureHTTPParser',
- );
- }
- }
- this.httpValidation = httpValidation;
-
- const requestTimeout = options.requestTimeout;
- if (requestTimeout !== undefined) {
- validateInteger(requestTimeout, 'requestTimeout', 0);
- this.requestTimeout = requestTimeout;
- } else {
- this.requestTimeout = 300_000; // 5 minutes
- }
-
- const headersTimeout = options.headersTimeout;
- if (headersTimeout !== undefined) {
- validateInteger(headersTimeout, 'headersTimeout', 0);
- this.headersTimeout = headersTimeout;
- } else {
- this.headersTimeout = MathMin(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout
- }
-
- if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) {
- throw new ERR_OUT_OF_RANGE('headersTimeout', '<= requestTimeout', headersTimeout);
- }
-
- const keepAliveTimeout = options.keepAliveTimeout;
- if (keepAliveTimeout !== undefined) {
- validateInteger(keepAliveTimeout, 'keepAliveTimeout', 0);
- this.keepAliveTimeout = keepAliveTimeout;
- } else {
- this.keepAliveTimeout = 65_000; // 65 seconds;
- }
-
- const keepAliveTimeoutBuffer = options.keepAliveTimeoutBuffer;
- if (keepAliveTimeoutBuffer !== undefined) {
- validateInteger(keepAliveTimeoutBuffer, 'keepAliveTimeoutBuffer', 0);
- this.keepAliveTimeoutBuffer = keepAliveTimeoutBuffer;
- } else {
- this.keepAliveTimeoutBuffer = 1000;
- }
-
- const connectionsCheckingInterval = options.connectionsCheckingInterval;
- if (connectionsCheckingInterval !== undefined) {
- validateInteger(connectionsCheckingInterval, 'connectionsCheckingInterval', 0);
- this.connectionsCheckingInterval = connectionsCheckingInterval;
- } else {
- this.connectionsCheckingInterval = 30_000; // 30 seconds
- }
-
- const requireHostHeader = options.requireHostHeader;
- if (requireHostHeader !== undefined) {
- validateBoolean(requireHostHeader, 'options.requireHostHeader');
- this.requireHostHeader = requireHostHeader;
- } else {
- this.requireHostHeader = true;
- }
-
- const joinDuplicateHeaders = options.joinDuplicateHeaders;
- if (joinDuplicateHeaders !== undefined) {
- validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
- }
- this.joinDuplicateHeaders = joinDuplicateHeaders;
-
- const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites;
- if (rejectNonStandardBodyWrites !== undefined) {
- validateBoolean(rejectNonStandardBodyWrites, 'options.rejectNonStandardBodyWrites');
- this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites;
- } else {
- this.rejectNonStandardBodyWrites = false;
- }
-
- const shouldUpgradeCallback = options.shouldUpgradeCallback;
- if (shouldUpgradeCallback !== undefined) {
- validateFunction(shouldUpgradeCallback, 'options.shouldUpgradeCallback');
- this.shouldUpgradeCallback = shouldUpgradeCallback;
- } else {
- this.shouldUpgradeCallback = function() {
- return this.listenerCount('upgrade') > 0;
- };
- }
-}
-
-function setupConnectionsTracking() {
- // Start connection handling
- this[kConnections] ||= new ConnectionsList();
-
- if (this[kConnectionsCheckingInterval]) {
- clearInterval(this[kConnectionsCheckingInterval]);
- }
- // This checker is started without checking whether any headersTimeout or requestTimeout is non zero
- // otherwise it would not be started if such timeouts are modified after createServer.
- this[kConnectionsCheckingInterval] =
- setInterval(checkConnections.bind(this), this.connectionsCheckingInterval).unref();
-}
-
-function httpServerPreClose(server) {
- server.closeIdleConnections();
- clearInterval(server[kConnectionsCheckingInterval]);
-}
-
-function Server(options, requestListener) {
- if (!(this instanceof Server)) {
- return deprecateInstantiation(Server, 'DEP0195', options, requestListener);
- }
-
- if (typeof options === 'function') {
- requestListener = options;
- options = kEmptyObject;
- } else if (options == null) {
- options = kEmptyObject;
- } else {
- validateObject(options, 'options');
- }
-
- storeHTTPOptions.call(this, options);
-
- net.Server.call(
- this,
- { allowHalfOpen: true, noDelay: options.noDelay ?? true,
- keepAlive: options.keepAlive,
- keepAliveInitialDelay: options.keepAliveInitialDelay,
- highWaterMark: options.highWaterMark });
-
- if (requestListener) {
- this.on('request', requestListener);
- }
-
- // Similar option to this. Too lazy to write my own docs.
- // http://www.squid-cache.org/Doc/config/half_closed_clients/
- // https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
- this.httpAllowHalfOpen = false;
-
- this.on('connection', connectionListener);
- this.on('listening', setupConnectionsTracking);
-
- this.timeout = 0;
- this.maxHeadersCount = null;
- this.maxRequestsPerSocket = 0;
-
- this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
-}
-ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
-ObjectSetPrototypeOf(Server, net.Server);
-
-Server.prototype.close = function close() {
- httpServerPreClose(this);
- ReflectApply(net.Server.prototype.close, this, arguments);
- return this;
-};
-
-Server.prototype[SymbolAsyncDispose] = assignFunctionName(SymbolAsyncDispose, async function() {
- await promisify(this.close).call(this);
-});
-
-Server.prototype.closeAllConnections = function closeAllConnections() {
- if (!this[kConnections]) {
- return;
- }
-
- const connections = this[kConnections].all();
-
- for (let i = 0, l = connections.length; i < l; i++) {
- connections[i].socket.destroy();
- }
-};
-
-Server.prototype.closeIdleConnections = function closeIdleConnections() {
- if (!this[kConnections]) {
- return;
- }
-
- const connections = this[kConnections].idle();
-
- for (let i = 0, l = connections.length; i < l; i++) {
- if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) {
- continue;
- }
-
- connections[i].socket.destroy();
- }
-};
-
-Server.prototype.setTimeout = function setTimeout(msecs, callback) {
- this.timeout = msecs;
- if (callback)
- this.on('timeout', callback);
- return this;
-};
-
-Server.prototype[EE.captureRejectionSymbol] =
-assignFunctionName(EE.captureRejectionSymbol, function(err, event, ...args) {
- switch (event) {
- case 'request': {
- const { 1: res } = args;
- if (!res.headersSent && !res.writableEnded) {
- // Don't leak headers.
- const names = res.getHeaderNames();
- for (let i = 0; i < names.length; i++) {
- res.removeHeader(names[i]);
- }
- res.statusCode = 500;
- res.end(STATUS_CODES[500]);
- } else {
- res.destroy();
- }
- break;
- }
- default:
- ReflectApply(
- net.Server.prototype[SymbolFor('nodejs.rejection')],
- this, arguments);
- }
-});
-
-function checkConnections() {
- const headersTimeout =
- NumberIsFinite(this.headersTimeout) && this.headersTimeout >= 0 ?
- this.headersTimeout : 0;
- const requestTimeout =
- NumberIsFinite(this.requestTimeout) && this.requestTimeout >= 0 ?
- this.requestTimeout : 0;
-
- if (headersTimeout === 0 && requestTimeout === 0) {
- return;
- }
-
- const expired = this[kConnections].expired(headersTimeout, requestTimeout);
-
- for (let i = 0; i < expired.length; i++) {
- const socket = expired[i].socket;
-
- if (socket) {
- onRequestTimeout(socket);
- }
- }
-}
-
-function connectionListener(socket) {
- defaultTriggerAsyncIdScope(
- getOrSetAsyncId(socket), connectionListenerInternal, this, socket,
- );
-}
-
-function connectionListenerInternal(server, socket) {
- debug('SERVER new http connection');
-
- // Ensure that the server property of the socket is correctly set.
- // See https://github.com/nodejs/node/issues/13435
- socket.server = server;
-
- // If the user has added a listener to the server,
- // request, or response, then it's their responsibility.
- // otherwise, destroy on timeout by default
- if (server.timeout && typeof socket.setTimeout === 'function')
- socket.setTimeout(server.timeout);
- socket.on('timeout', socketOnTimeout);
-
- const parser = parsers.alloc();
-
- const lenientFlags = calculateLenientFlags(server.httpValidation, server.insecureHTTPParser);
-
- // Propagate headers limit from server instance to parser
- if (typeof server.maxHeadersCount === 'number') {
- parser.maxHeaderPairs = server.maxHeadersCount << 1;
- }
-
- // TODO(addaleax): This doesn't play well with the
- // `async_hooks.currentResource()` proposal, see
- // https://github.com/nodejs/node/pull/21313
- parser.initialize(
- HTTPParser.REQUEST,
- new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket),
- server.maxHeaderSize || 0,
- lenientFlags,
- server[kConnections],
- parser.maxHeaderPairs,
- );
- parser.socket = socket;
- socket.parser = parser;
-
- const state = {
- onData: null,
- onEnd: null,
- onClose: null,
- onDrain: null,
- outgoing: [],
- incoming: [],
- // `outgoingData` is an approximate amount of bytes queued through all
- // inactive responses. If more data than the high watermark is queued - we
- // need to pause TCP socket/HTTP parser, and wait until the data will be
- // sent to the client.
- 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,
- server, socket, parser, state);
- state.onClose = socketOnClose.bind(undefined,
- socket, state);
- state.onDrain = socketOnDrain.bind(undefined,
- socket, state);
- socket.on('data', state.onData);
- socket.on('error', socketOnError);
- socket.on('end', state.onEnd);
- socket.on('close', state.onClose);
- socket.on('drain', state.onDrain);
- parser.onIncoming = parserOnIncoming.bind(undefined,
- server, socket, state);
-
- // We are consuming socket, so it won't get any actual data
- socket.on('resume', onSocketResume);
- socket.on('pause', onSocketPause);
-
- // Overrides to unconsume on `data`, `readable` listeners
- socket.on = generateSocketListenerWrapper('on');
- socket.addListener = generateSocketListenerWrapper('addListener');
- socket.prependListener = generateSocketListenerWrapper('prependListener');
- socket.setEncoding = socketSetEncoding;
-
- // We only consume the socket if it has never been consumed before.
- if (socket._handle?.isStreamBase &&
- !socket._handle._consumed) {
- parser._consumed = true;
- socket._handle._consumed = true;
- parser.consume(socket._handle);
- }
- parser[kOnExecute] =
- onParserExecute.bind(undefined,
- server, socket, parser, state);
-
- parser[kOnTimeout] =
- onParserTimeout.bind(undefined,
- server, socket);
-
- socket._paused = false;
-}
-
-function socketSetEncoding() {
- throw new ERR_HTTP_SOCKET_ENCODING();
-}
-
-function updateOutgoingData(socket, state, delta) {
- state.outgoingData += delta;
- socketOnDrain(socket, state);
-}
-
-function socketOnDrain(socket, state) {
- const needPause = state.outgoingData > socket.writableHighWaterMark;
-
- // If we previously paused, then start reading again.
- if (socket._paused && !needPause) {
- socket._paused = false;
- if (socket.parser)
- socket.parser.resume();
- socket.resume();
- }
-
- const msg = socket._httpMessage;
- // Only emit 'drain' once the message has no data pending anywhere, so that
- // msg.writableLength === 0 when the event fires. socketOnDrain is called
- // synchronously from updateOutgoingData during _flushOutput, at which point
- // the bytes we just handed to the socket (or the stale outputSize) mean
- // the message is not actually drained yet - we wait for the socket's
- // own 'drain' event instead.
- if (msg && !msg.finished && msg[kNeedDrain] && msg.writableLength === 0) {
- msg[kNeedDrain] = false;
- msg.emit('drain');
- }
-}
-
-function socketOnTimeout() {
- const req = this.parser?.incoming;
- const reqTimeout = req && !req.complete && req.emit('timeout', this);
- const res = this._httpMessage;
- const resTimeout = res && res.emit('timeout', this);
- const serverTimeout = this.server.emit('timeout', this);
-
- if (!reqTimeout && !resTimeout && !serverTimeout)
- this.destroy();
-}
-
-function socketOnClose(socket, state) {
- debug('server socket close');
- freeParser(socket.parser, null, socket);
- abortIncoming(state.incoming);
- abortOutgoing(state.outgoing);
-}
-
-function abortIncoming(incoming) {
- while (incoming.length) {
- const req = incoming.shift();
- req.destroy(new ConnResetException('aborted'));
- }
-}
-
-function abortOutgoing(outgoing) {
- while (outgoing.length) {
- const req = outgoing.shift();
- req.destroy(new ConnResetException('aborted'));
- }
-}
-
-function socketOnEnd(server, socket, parser, state) {
- const ret = parser.finish();
-
- if (ret instanceof Error) {
- debug('parse error');
- // socketOnError has additional logic and will call socket.destroy(err).
- socketOnError.call(socket, ret);
- } else if (!server.httpAllowHalfOpen) {
- socket.end();
- } else if (state.outgoing.length) {
- state.outgoing[state.outgoing.length - 1]._last = true;
- } else if (socket._httpMessage) {
- socket._httpMessage._last = true;
- } else {
- socket.end();
- }
-}
-
-function socketOnData(server, socket, parser, state, d) {
- assert(!socket._paused);
- debug('SERVER socketOnData %d', d.length);
-
- const ret = parser.execute(d);
- onParserExecuteCommon(server, socket, parser, state, ret, d);
-}
-
-function onRequestTimeout(socket) {
- // socketOnError has additional logic and will call socket.destroy(err).
- socketOnError.call(socket, new ERR_HTTP_REQUEST_TIMEOUT());
-}
-
-function onParserExecute(server, socket, parser, state, ret) {
- // When underlying `net.Socket` instance is consumed - no
- // `data` events are emitted, and thus `socket.setTimeout` fires the
- // callback even if the data is constantly flowing into the socket.
- // See, https://github.com/nodejs/node/commit/ec2822adaad76b126b5cccdeaa1addf2376c9aa6
- socket._unrefTimer();
- debug('SERVER socketOnParserExecute %d', ret);
- onParserExecuteCommon(server, socket, parser, state, ret, undefined);
-}
-
-function onParserTimeout(server, socket) {
- const serverTimeout = server.emit('timeout', socket);
-
- if (!serverTimeout)
- socket.destroy();
-}
-
-const noop = () => {};
-const badRequestResponse = Buffer.from(
- `HTTP/1.1 400 ${STATUS_CODES[400]}\r\n` +
- 'Connection: close\r\n\r\n', 'ascii',
-);
-const requestTimeoutResponse = Buffer.from(
- `HTTP/1.1 408 ${STATUS_CODES[408]}\r\n` +
- 'Connection: close\r\n\r\n', 'ascii',
-);
-const requestHeaderFieldsTooLargeResponse = Buffer.from(
- `HTTP/1.1 431 ${STATUS_CODES[431]}\r\n` +
- 'Connection: close\r\n\r\n', 'ascii',
-);
-
-const requestChunkExtensionsTooLargeResponse = Buffer.from(
- `HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` +
- 'Connection: close\r\n\r\n', 'ascii',
-);
-
-function socketOnError(e) {
- // Ignore further errors
- this.removeListener('error', socketOnError);
-
- if (this.listenerCount('error', noop) === 0) {
- this.on('error', noop);
- }
-
- if (!this.server.emit('clientError', e, this)) {
- // Caution must be taken to avoid corrupting the remote peer.
- // Reply an error segment if there is no in-flight `ServerResponse`,
- // or no data of the in-flight one has been written yet to this socket.
- if (this.writable &&
- (!this._httpMessage || !this._httpMessage._headerSent)) {
- let response;
-
- switch (e.code) {
- case 'HPE_HEADER_OVERFLOW':
- response = requestHeaderFieldsTooLargeResponse;
- break;
- case 'HPE_CHUNK_EXTENSIONS_OVERFLOW':
- response = requestChunkExtensionsTooLargeResponse;
- break;
- case 'ERR_HTTP_REQUEST_TIMEOUT':
- response = requestTimeoutResponse;
- break;
- default:
- response = badRequestResponse;
- break;
- }
-
- this.write(response);
- }
- this.destroy(e);
- }
-}
-
-class UpgradeStream extends Duplex {
- constructor(socket, req) {
- super({
- allowHalfOpen: socket.allowHalfOpen,
- });
-
- this[kSocket] = socket;
- this[kIncomingMessage] = req;
-
- // Proxy error, end & closure events immediately.
- socket.on('error', (err) => this.destroy(err));
-
- socket.on('close', () => this.destroy());
- this.on('close', () => socket.destroy());
-
- socket.on('end', () => {
- this.push(null);
-
- // Match the socket behaviour, where 'end' will fire despite no 'data'
- // listeners if a socket with no pending data ends:
- if (this.readableLength === 0) {
- this.resume();
- }
- });
-
- // Other events (most notably, reading) all only
- // activate after requestBodyCompleted is called.
- }
-
- requestBodyCompleted(upgradeHead) {
- this[kIncomingMessage] = null;
-
- // When the request body is completed, we begin streaming all the
- // post-body data for the upgraded protocol:
- if (upgradeHead?.length > 0) {
- if (!this.push(upgradeHead)) {
- this[kSocket].pause();
- }
- }
-
- this[kSocket].on('data', (data) => {
- if (!this.push(data)) {
- this[kSocket].pause();
- }
- });
- }
-
- _read(size) {
- // Reading the upgrade stream starts the request stream flowing. It's
- // important that this happens, even if there are no listeners, or it
- // would be impossible to read this without explicitly reading all the
- // request body first, which is backward incompatible & awkward.
- this[kIncomingMessage]?.resume();
-
- this[kSocket].resume();
- }
-
- _final(callback) {
- this[kSocket].end(callback);
- }
-
- _write(chunk, encoding, callback) {
- this[kSocket].write(chunk, encoding, callback);
- }
-
- _destroy(err, callback) {
- this[kSocket].destroy(err);
- callback(err);
- }
-}
-
-function onParserExecuteCommon(server, socket, parser, state, ret, d) {
- if (ret instanceof Error) {
- prepareError(ret, parser, d);
- debug('parse error', ret);
- socketOnError.call(socket, ret);
- } else if (parser.incoming?.upgrade) {
- // Upgrade or CONNECT
- const req = parser.incoming;
- debug('SERVER upgrade or connect', req.method);
- const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
-
- let upgradeStream;
- if (req.complete) {
- d ||= parser.getCurrentBuffer();
-
- socket.removeListener('data', state.onData);
- socket.removeListener('end', state.onEnd);
- socket.removeListener('close', state.onClose);
- socket.removeListener('drain', state.onDrain);
- socket.removeListener('error', socketOnError);
- socket.removeListener('timeout', socketOnTimeout);
-
- unconsume(parser, socket);
- parser.finish();
- freeParser(parser, req, socket);
- parser = null;
-
- // If the request is complete (no body, or all body read upfront) then
- // we just emit the socket directly as the upgrade stream.
- upgradeStream = socket;
- } else {
- // If the body hasn't been fully parsed yet, we emit immediately but
- // we add a wrapper around the socket to not expose incoming data
- // until the request body has finished.
-
- if (socket[kUpgradeStream]) {
- // We've already emitted the incomplete upgrade - nothing do to
- // until actual body parsing completion.
- return;
- }
-
- d ||= Buffer.alloc(0);
-
- upgradeStream = new UpgradeStream(socket, req);
- socket[kUpgradeStream] = upgradeStream;
- }
-
- if (server.listenerCount(eventName) > 0) {
- debug('SERVER have listener for %s', eventName);
-
- const bodyHead = d.slice(ret, d.length);
-
- if (req.complete && socket[kUpgradeStream]) {
- // Previously emitted, now completed - just activate the stream
- socket[kUpgradeStream].requestBodyCompleted(bodyHead);
- } else {
- socket.readableFlowing = null;
- server.emit(eventName, req, upgradeStream, bodyHead);
- }
- } else {
- // Got upgrade or CONNECT method, but have no handler.
- socket.destroy();
- }
- } else if (parser.incoming && parser.incoming.method === 'PRI') {
- debug('SERVER got PRI request');
- socket.destroy();
- }
-
- if (socket._paused && socket.parser) {
- // onIncoming paused the socket, we should pause the parser as well
- debug('pause parser');
- socket.parser.pause();
- }
-}
-
-function clearIncoming(req) {
- req ||= this;
- const parser = req.socket?.parser;
- // Reset the .incoming property so that the request object can be gc'ed.
- if (parser && parser.incoming === req) {
- if (req.readableEnded) {
- parser.incoming = null;
- } else {
- req.on('end', clearIncoming);
- }
- }
-}
-
-function resOnFinish(req, res, socket, state, server) {
- if (onResponseFinishChannel.hasSubscribers) {
- onResponseFinishChannel.publish({
- request: req,
- response: res,
- socket,
- server,
- });
- }
-
- // Usually the first incoming element should be our request. it may
- // be that in the case abortIncoming() was called that the incoming
- // array will be empty.
- assert(state.incoming.length === 0 || state.incoming[0] === req);
-
- req[kDetachAbortSignal]();
- state.incoming.shift();
-
- // If the user never called req.read(), and didn't pipe() or
- // .resume() or .on('data'), then we call req._dump() so that the
- // bytes will be pulled off the wire.
- if (!req._consuming && !req._readableState.resumeScheduled)
- req._dump();
-
- res.detachSocket(socket);
- clearIncoming(req);
- process.nextTick(emitCloseNT, res);
-
- if (res._last) {
- if (typeof socket.destroySoon === 'function') {
- socket.destroySoon();
- } else {
- socket.end();
- }
- } else if (state.outgoing.length === 0) {
- const keepAliveTimeout = NumberIsFinite(server.keepAliveTimeout) && server.keepAliveTimeout >= 0 ?
- server.keepAliveTimeout : 0;
- const keepAliveTimeoutBuffer = NumberIsFinite(server.keepAliveTimeoutBuffer) && server.keepAliveTimeoutBuffer >= 0 ?
- server.keepAliveTimeoutBuffer : 1e3;
-
- if (keepAliveTimeout && typeof socket.setTimeout === 'function') {
- // Extend the internal timeout by the configured buffer to reduce
- // the likelihood of ECONNRESET errors.
- // This allows fine-tuning beyond the advertised keepAliveTimeout.
- socket.setTimeout(keepAliveTimeout + keepAliveTimeoutBuffer);
- state.keepAliveTimeoutSet = true;
- }
- } else {
- // Start sending the next message
- const m = state.outgoing.shift();
- if (m) {
- m.assignSocket(socket);
- }
- }
-}
-
-function emitCloseNT(self) {
- if (!self._closed) {
- self.destroyed = true;
- self._closed = true;
- self.emit('close');
- }
-}
-
-// 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 && asciiEqualIgnoreCase(key, 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 key = rawHeaders[i];
- const length = key.length;
- if (length === 14) {
- if (asciiEqualIgnoreCase(key, 'content-length'))
- return true;
- } else if (length === 17) {
- if (asciiEqualIgnoreCase(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
-// new message. In this callback we setup the response object and pass it
-// to the user.
-function parserOnIncoming(server, socket, state, req, keepAlive) {
- resetSocketTimeout(server, socket, state);
-
- if (req.upgrade) {
- req.upgrade = req.method === 'CONNECT' ||
- !!server.shouldUpgradeCallback(req);
- if (req.upgrade) {
- return 0;
- }
- }
-
- state.incoming.push(req);
-
- // If the writable end isn't consuming, then stop reading
- // so that we don't become overwhelmed by a flood of
- // pipelined requests that may never be resolved.
- if (!socket._paused) {
- const ws = socket._writableState;
- if (ws.needDrain || state.outgoingData >= socket.writableHighWaterMark) {
- socket._paused = true;
- // We also need to pause the parser, but don't do that until after
- // the call to execute, because we may still be processing the last
- // chunk.
- socket.pause();
- }
- }
-
- const res = new server[kServerResponse](req, getResponseOptions(server, socket));
- res._keepAliveTimeout = server.keepAliveTimeout;
- res._maxRequestsPerSocket = server.maxRequestsPerSocket;
- res._onPendingData = state.onPendingData;
- res[kConnectionState] = state;
-
- res.shouldKeepAlive = keepAlive;
- res[kUniqueHeaders] = server[kUniqueHeaders];
-
- if (onRequestStartChannel.hasSubscribers) {
- onRequestStartChannel.publish({
- request: req,
- response: res,
- socket,
- server,
- });
- }
-
- // Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
- const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req);
-
- if (shouldOptimize) {
- // Fast processing where emitting 'data', 'end' and 'close' events is
- // skipped and data is dumped.
- // This avoids a lot of unnecessary overhead otherwise introduced by
- // stream.Readable life cycle rules. The downside is that this will
- // break some servers that read bodies for methods that don't have body headers.
- req._dumpAndCloseReadable();
- req._read();
- }
-
- if (socket._httpMessage) {
- // There are already pending outgoing res, append.
- state.outgoing.push(res);
- } else {
- res.assignSocket(socket);
- }
-
- // When we're finished writing the response, check if this is the last
- // response, if so destroy the socket.
- res.on('finish', onResponseFinish);
-
- let handled = false;
-
-
- if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
-
- // 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 && !hasRequestHeader(req, 4, 'host')) {
- res.writeHead(400, ['Connection', 'close']);
- res.end();
- return 0;
- }
-
- const isRequestsLimitSet = (
- typeof server.maxRequestsPerSocket === 'number' &&
- server.maxRequestsPerSocket > 0
- );
-
- if (isRequestsLimitSet) {
- state.requestsCount++;
- res.maxRequestsOnConnectionReached = (
- server.maxRequestsPerSocket <= state.requestsCount);
- }
-
- if (isRequestsLimitSet &&
- (server.maxRequestsPerSocket < state.requestsCount)) {
- handled = true;
- server.emit('dropRequest', req, socket);
- res.writeHead(503);
- res.end();
- } else {
- const expect = getRawHeader(req, 'expect', true);
- if (expect !== undefined) {
- handled = true;
-
- if (continueExpression.test(expect)) {
- res._expect_continue = true;
- if (server.listenerCount('checkContinue') > 0) {
- server.emit('checkContinue', req, res);
- } else {
- res.writeContinue();
- server.emit('request', req, res);
- }
- } else if (server.listenerCount('checkExpectation') > 0) {
- server.emit('checkExpectation', req, res);
- } else {
- res.writeHead(417);
- res.end();
- }
- }
- }
- }
-
- if (!handled) {
- server.emit('request', req, res);
- }
-
- return 0; // No special treatment.
-}
-
-function resetSocketTimeout(server, socket, state) {
- if (!state.keepAliveTimeoutSet)
- return;
-
- socket.setTimeout(server.timeout || 0);
- state.keepAliveTimeoutSet = false;
-}
-
-function onSocketResume() {
- // It may seem that the socket is resumed, but this is an enemy's trick to
- // deceive us! `resume` is emitted asynchronously, and may be called from
- // `incoming.readStart()`. Stop the socket again here, just to preserve the
- // state.
- //
- // We don't care about stream semantics for the consumed socket anyway.
- if (this._paused) {
- this.pause();
- return;
- }
-
- if (this._handle && !this._handle.reading) {
- this._handle.reading = true;
- this._handle.readStart();
- }
-}
-
-function onSocketPause() {
- if (this._handle?.reading) {
- this._handle.reading = false;
- this._handle.readStop();
- }
-}
-
-function unconsume(parser, socket) {
- if (socket._handle) {
- if (parser._consumed)
- parser.unconsume();
- parser._consumed = false;
- socket.removeListener('pause', onSocketPause);
- socket.removeListener('resume', onSocketResume);
- }
-}
-
-function generateSocketListenerWrapper(originalFnName) {
- return function socketListenerWrap(ev, fn) {
- const res = net.Socket.prototype[originalFnName].call(this,
- ev, fn);
- if (!this.parser) {
- this.on = net.Socket.prototype.on;
- this.addListener = net.Socket.prototype.addListener;
- this.prependListener = net.Socket.prototype.prependListener;
- return res;
- }
-
- if (ev === 'data' || ev === 'readable')
- unconsume(this.parser, this);
+ STATUS_CODES,
+ Server,
+ ServerResponse,
+ setupConnectionsTracking,
+ storeHTTPOptions,
+ _connectionListener,
+ kServerResponse,
+ httpServerPreClose,
+ kConnectionsCheckingInterval,
+} = require('internal/http/server');
- return res;
- };
-}
module.exports = {
STATUS_CODES,
@@ -1542,8 +40,11 @@ module.exports = {
ServerResponse,
setupConnectionsTracking,
storeHTTPOptions,
- _connectionListener: connectionListener,
+ _connectionListener,
kServerResponse,
httpServerPreClose,
kConnectionsCheckingInterval,
};
+
+process.emitWarning('The _http_server module is deprecated. Use `node:http` instead.',
+ 'DeprecationWarning', 'DEP0199');
diff --git a/lib/http.js b/lib/http.js
index 726e3e30933..bc99ba61e37 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -27,10 +27,10 @@ const {
} = primordials;
const { validateInteger, validateObject } = require('internal/validators');
-const httpAgent = require('_http_agent');
-const { ClientRequest } = require('_http_client');
-const { methods, parsers } = require('_http_common');
-const { IncomingMessage } = require('_http_incoming');
+const httpAgent = require('internal/http/agent');
+const { ClientRequest } = require('internal/http/client');
+const { methods, parsers } = require('internal/http/common');
+const { IncomingMessage } = require('internal/http/incoming');
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
@@ -48,13 +48,13 @@ const {
validateHeaderName,
validateHeaderValue,
OutgoingMessage,
-} = require('_http_outgoing');
+} = require('internal/http/outgoing');
const {
_connectionListener,
STATUS_CODES,
Server,
ServerResponse,
-} = require('_http_server');
+} = require('internal/http/server');
const {
parseProxyUrl,
getGlobalAgent,
diff --git a/lib/https.js b/lib/https.js
index dc9df0da184..454e923cc34 100644
--- a/lib/https.js
+++ b/lib/https.js
@@ -57,15 +57,15 @@ const {
kWaitForProxyTunnel,
getGlobalAgent,
} = require('internal/http');
-const { Agent: HttpAgent } = require('_http_agent');
+const { Agent: HttpAgent } = require('internal/http/agent');
const {
httpServerPreClose,
Server: HttpServer,
setupConnectionsTracking,
storeHTTPOptions,
_connectionListener,
-} = require('_http_server');
-const { ClientRequest } = require('_http_client');
+} = require('internal/http/server');
+const { ClientRequest } = require('internal/http/client');
let debug = require('internal/util/debuglog').debuglog('https', (fn) => {
debug = fn;
});
diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js
index 06daf2183db..363825061dd 100644
--- a/lib/internal/child_process.js
+++ b/lib/internal/child_process.js
@@ -194,9 +194,9 @@ const handleConversion = {
socket.setTimeout(0);
if (freeParser === undefined)
- freeParser = require('_http_common').freeParser;
+ freeParser = require('internal/http/common').freeParser;
if (HTTPParser === undefined)
- HTTPParser = require('_http_common').HTTPParser;
+ HTTPParser = require('internal/http/common').HTTPParser;
// In case of an HTTP connection socket, release the associated
// resources
diff --git a/lib/internal/http/agent.js b/lib/internal/http/agent.js
new file mode 100644
index 00000000000..3871c5b56a2
--- /dev/null
+++ b/lib/internal/http/agent.js
@@ -0,0 +1,715 @@
+// 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 {
+ NumberIsFinite,
+ NumberParseInt,
+ ObjectKeys,
+ ObjectSetPrototypeOf,
+ ObjectValues,
+ Symbol,
+} = primordials;
+
+const net = require('net');
+const EventEmitter = require('events');
+let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
+ debug = fn;
+});
+const {
+ parseProxyConfigFromEnv,
+ kProxyConfig,
+ checkShouldUseProxy,
+ kWaitForProxyTunnel,
+ getGlobalAgent,
+} = require('internal/http');
+const { AsyncResource } = require('async_hooks');
+const {
+ async_id_symbol,
+ owner_symbol,
+} = require('internal/async_hooks').symbols;
+const {
+ deprecateInstantiation,
+ getLazy,
+ kEmptyObject,
+ once,
+} = require('internal/util');
+const {
+ onStreamRead,
+} = require('internal/stream_base_commons');
+const {
+ kReadBytesOrError,
+ streamBaseState,
+} = internalBinding('stream_wrap');
+const {
+ validateNumber,
+ validateOneOf,
+ validateString,
+} = require('internal/validators');
+const assert = require('internal/assert');
+const { getOptionValue } = require('internal/options');
+
+const kOnKeylog = Symbol('onkeylog');
+const kRequestOptions = Symbol('requestOptions');
+const kRequestAsyncResource = Symbol('requestAsyncResource');
+const kFreeSocketDataGuard = Symbol('freeSocketDataGuard');
+
+// New Agent code.
+
+// The largest departure from the previous implementation is that
+// an Agent instance holds connections for a variable number of host:ports.
+// Surprisingly, this is still API compatible as far as third parties are
+// concerned. The only code that really notices the difference is the
+// request object.
+
+// Another departure is that all code related to HTTP parsing is in
+// ClientRequest.onSocket(). The Agent is now *strictly*
+// concerned with managing a connection pool.
+
+class ReusedHandle {
+ constructor(type, handle) {
+ this.type = type;
+ this.handle = handle;
+ }
+}
+
+function freeSocketErrorListener(err) {
+ const socket = this;
+ debug('SOCKET ERROR on FREE socket:', err.message, err.stack);
+ socket.destroy();
+ socket.emit('agentRemove');
+}
+
+// Guard against unsolicited data arriving while a socket is idle in the
+// freeSockets pool. When the HTTPParser is detached the data would sit
+// in the TCP buffer and be silently consumed as the response for the
+// *next* request that reuses the socket (response-queue poisoning).
+// See: https://hackerone.com/reports/3582376
+function freeSocketOnReadGuard() {
+ const nread = streamBaseState[kReadBytesOrError];
+ if (nread === 0) return;
+
+ debug('READ on FREE socket - destroying poisoned socket');
+ this[owner_symbol].destroy();
+}
+
+function installFreeSocketDataGuard(socket) {
+ if (socket.readableLength > 0) {
+ debug('BUFFERED DATA on FREE socket - destroying poisoned socket');
+ socket.destroy();
+ return;
+ }
+
+ if (socket.connecting) {
+ socket[kFreeSocketDataGuard] = function onConnect() {
+ socket[kFreeSocketDataGuard] = null;
+ installFreeSocketDataGuard(socket);
+ };
+ socket.once('connect', socket[kFreeSocketDataGuard]);
+ return;
+ }
+
+ const handle = socket._handle;
+ if (handle) {
+ handle.onread = freeSocketOnReadGuard;
+ if (!handle.reading) {
+ handle.reading = true;
+ const err = handle.readStart();
+ if (err) socket.destroy();
+ }
+ }
+}
+
+function removeFreeSocketDataGuard(socket) {
+ if (socket[kFreeSocketDataGuard]) {
+ socket.removeListener('connect', socket[kFreeSocketDataGuard]);
+ socket[kFreeSocketDataGuard] = null;
+ }
+
+ const handle = socket._handle;
+ if (handle?.onread === freeSocketOnReadGuard) {
+ handle.onread = onStreamRead;
+ }
+}
+
+function Agent(options) {
+ if (!(this instanceof Agent)) {
+ return deprecateInstantiation(Agent, 'DEP0195', options);
+ }
+
+ EventEmitter.call(this);
+
+ this.options = { __proto__: null, ...options };
+
+ this.defaultPort = this.options.defaultPort || 80;
+ this.protocol = this.options.protocol || 'http:';
+
+ if (this.options.noDelay === undefined)
+ this.options.noDelay = true;
+
+ // Don't confuse net and make it think that we're connecting to a pipe
+ this.options.path = null;
+ this.requests = { __proto__: null };
+ this.sockets = { __proto__: null };
+ this.freeSockets = { __proto__: null };
+ this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;
+ this.keepAlive = this.options.keepAlive || false;
+ this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
+ this.maxFreeSockets = this.options.maxFreeSockets || 256;
+ this.scheduling = this.options.scheduling || 'lifo';
+ this.maxTotalSockets = this.options.maxTotalSockets;
+ this.totalSocketCount = 0;
+
+ this.agentKeepAliveTimeoutBuffer =
+ typeof this.options.agentKeepAliveTimeoutBuffer === 'number' &&
+ this.options.agentKeepAliveTimeoutBuffer >= 0 &&
+ NumberIsFinite(this.options.agentKeepAliveTimeoutBuffer) ?
+ this.options.agentKeepAliveTimeoutBuffer :
+ 1000;
+
+ const proxyEnv = this.options.proxyEnv;
+ if (typeof proxyEnv === 'object' && proxyEnv !== null) {
+ this[kProxyConfig] = parseProxyConfigFromEnv(proxyEnv, this.protocol, this.keepAlive);
+ debug(`new ${this.protocol} agent with proxy config`, this[kProxyConfig]);
+ }
+
+ validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']);
+
+ if (this.maxTotalSockets !== undefined) {
+ validateNumber(this.maxTotalSockets, 'maxTotalSockets', 1);
+ } else {
+ this.maxTotalSockets = Infinity;
+ }
+
+ this.on('free', (socket, options) => {
+ const name = this.getName(options);
+ debug('agent.on(free)', name);
+
+ // TODO(ronag): socket.destroy(err) might have been called
+ // before coming here and have an 'error' scheduled. In the
+ // case of socket.destroy() below this 'error' has no handler
+ // and could cause unhandled exception.
+
+ if (!socket.writable) {
+ socket.destroy();
+ return;
+ }
+
+ const requests = this.requests[name];
+ if (requests?.length) {
+ const req = requests.shift();
+ const reqAsyncRes = req[kRequestAsyncResource];
+ if (reqAsyncRes) {
+ // Run request within the original async context.
+ reqAsyncRes.runInAsyncScope(() => {
+ asyncResetHandle(socket);
+ setRequestSocket(this, req, socket);
+ });
+ req[kRequestAsyncResource] = null;
+ } else {
+ setRequestSocket(this, req, socket);
+ }
+ if (requests.length === 0) {
+ delete this.requests[name];
+ }
+ return;
+ }
+
+ // If there are no pending requests, then put it in
+ // the freeSockets pool, but only if we're allowed to do so.
+ const req = socket._httpMessage;
+ if (!req || !req.shouldKeepAlive || !this.keepAlive) {
+ socket.destroy();
+ return;
+ }
+
+ const freeSockets = this.freeSockets[name] || [];
+ const freeLen = freeSockets.length;
+ let count = freeLen;
+ if (this.sockets[name])
+ count += this.sockets[name].length;
+
+ if (this.totalSocketCount > this.maxTotalSockets ||
+ count > this.maxSockets ||
+ freeLen >= this.maxFreeSockets ||
+ !this.keepSocketAlive(socket)) {
+ socket.destroy();
+ return;
+ }
+
+ this.freeSockets[name] = freeSockets;
+ socket[async_id_symbol] = -1;
+ socket._httpMessage = null;
+ this.removeSocket(socket, options);
+
+ socket.once('error', freeSocketErrorListener);
+ installFreeSocketDataGuard(socket);
+ freeSockets.push(socket);
+ });
+
+ // Don't emit keylog events unless there is a listener for them.
+ this.on('newListener', maybeEnableKeylog);
+}
+ObjectSetPrototypeOf(Agent.prototype, EventEmitter.prototype);
+ObjectSetPrototypeOf(Agent, EventEmitter);
+
+function maybeEnableKeylog(eventName) {
+ if (eventName === 'keylog') {
+ this.removeListener('newListener', maybeEnableKeylog);
+ // Future sockets will listen on keylog at creation.
+ const agent = this;
+ this[kOnKeylog] = function onkeylog(keylog) {
+ agent.emit('keylog', keylog, this);
+ };
+ // Existing sockets will start listening on keylog now. Both maps hold
+ // arrays of sockets keyed by name, so each bucket has to be walked.
+ const sets = [this.freeSockets, this.sockets];
+ for (let s = 0; s < sets.length; s++) {
+ const buckets = ObjectValues(sets[s]);
+ for (let b = 0; b < buckets.length; b++) {
+ const sockets = buckets[b];
+ for (let n = 0; n < sockets.length; n++) {
+ sockets[n].on('keylog', this[kOnKeylog]);
+ }
+ }
+ }
+ }
+}
+
+const lazyTLS = getLazy(() => require('tls'));
+
+Agent.defaultMaxSockets = Infinity;
+
+// See ProxyConfig in internal/http.js for how the connection should be handled
+// when the agent is configured to use a proxy server.
+Agent.prototype.createConnection = function createConnection(...args) {
+ const normalized = net._normalizeArgs(args);
+ const options = normalized[0];
+ const cb = normalized[1];
+
+ // Check if this specific request should bypass the proxy
+ const shouldUseProxy = checkShouldUseProxy(this[kProxyConfig], options);
+ debug(`http createConnection should use proxy for ${options.host}:${options.port}:`, shouldUseProxy);
+ if (!shouldUseProxy) { // Forward to net.createConnection if no proxying is needed.
+ return net.createConnection(...args);
+ }
+
+ // Create a copy of the shared proxy connection options and connect
+ // to the proxy server instead of the endpoint. For Agent.prototype.createConnection
+ // which is used by the http agent, this is enough
+ const connectOptions = {
+ ...this[kProxyConfig].proxyConnectionOptions,
+ };
+ const proxyProtocol = this[kProxyConfig].protocol;
+ if (proxyProtocol === 'http:') {
+ return net.connect(connectOptions, cb);
+ } else if (proxyProtocol === 'https:') {
+ return lazyTLS().connect(connectOptions, cb);
+ }
+ // This should be unreachable because proxy config should be null for other protocols.
+ assert.fail(`Unexpected proxy protocol ${proxyProtocol}`);
+
+};
+
+// Get the key for a given set of request options
+Agent.prototype.getName = function getName(options = kEmptyObject) {
+ let name = options.host || 'localhost';
+
+ name += ':';
+ if (options.port)
+ name += options.port;
+
+ name += ':';
+ if (options.localAddress)
+ name += options.localAddress;
+
+ // Pacify parallel/test-http-agent-getname by only appending
+ // the ':' when options.family is set.
+ if (options.family === 4 || options.family === 6)
+ name += `:${options.family}`;
+
+ if (options.socketPath)
+ name += `:${options.socketPath}`;
+
+ return name;
+};
+
+function handleSocketAfterProxy(err, req) {
+ if (err.code === 'ERR_PROXY_TUNNEL') {
+ if (err.proxyTunnelTimeout) {
+ req.emit('timeout'); // Propagate the timeout from the tunnel to the request.
+ }
+ req.emit('error', err);
+ }
+}
+
+Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
+ localAddress/* legacy */) {
+ // Legacy API: addRequest(req, host, port, localAddress)
+ if (typeof options === 'string') {
+ options = {
+ __proto__: null,
+ host: options,
+ port,
+ localAddress,
+ };
+ }
+
+ // Here the agent options will override per-request options.
+ options = { __proto__: null, ...options, ...this.options };
+ if (options.socketPath)
+ options.path = options.socketPath;
+
+ normalizeServerName(options, req);
+
+ const name = this.getName(options);
+ this.sockets[name] ||= [];
+
+ const freeSockets = this.freeSockets[name];
+ let socket;
+ if (freeSockets) {
+ while (freeSockets.length && freeSockets[0].destroyed) {
+ freeSockets.shift();
+ }
+ socket = this.scheduling === 'fifo' ?
+ freeSockets.shift() :
+ freeSockets.pop();
+ if (!freeSockets.length)
+ delete this.freeSockets[name];
+ }
+
+ const freeLen = freeSockets ? freeSockets.length : 0;
+ const sockLen = freeLen + this.sockets[name].length;
+
+ // Reusing a socket from the pool.
+ // If the caller specified a highWaterMark that differs from the pooled
+ // socket's writableHighWaterMark, sync the socket's HWM so that
+ // backpressure semantics match what the caller requested.
+ if (socket && options.highWaterMark != null &&
+ socket.writableHighWaterMark !== options.highWaterMark) {
+ debug('sync reused socket HWM (socket=%d, request=%d)',
+ socket.writableHighWaterMark, options.highWaterMark);
+ socket._writableState.highWaterMark = options.highWaterMark;
+ }
+
+ if (socket) {
+ asyncResetHandle(socket);
+ this.reuseSocket(socket, req);
+ setRequestSocket(this, req, socket);
+ this.sockets[name].push(socket);
+ } else if (sockLen < this.maxSockets &&
+ this.totalSocketCount < this.maxTotalSockets) {
+ // If we are under maxSockets create a new one.
+ this.createSocket(req, options, (err, socket) => {
+ if (err) {
+ handleSocketAfterProxy(err, req);
+ debug('call onSocket', sockLen, freeLen);
+ req.onSocket(socket, err);
+ return;
+ }
+
+ setRequestSocket(this, req, socket);
+ });
+ } else {
+ debug('wait for socket');
+ // We are over limit so we'll add it to the queue.
+ this.requests[name] ||= [];
+
+ // Used to create sockets for pending requests from different origin
+ req[kRequestOptions] = options;
+ // Used to capture the original async context.
+ req[kRequestAsyncResource] = new AsyncResource('QueuedRequest');
+
+ this.requests[name].push(req);
+ }
+};
+
+Agent.prototype.createSocket = function createSocket(req, options, cb) {
+ // Here the agent options will override per-request options.
+ options = { __proto__: null, ...options, ...this.options };
+ if (options.socketPath)
+ options.path = options.socketPath;
+
+ normalizeServerName(options, req);
+
+ // Make sure per-request timeout is respected.
+ const timeout = req.timeout || this.options.timeout || undefined;
+ if (timeout) {
+ options.timeout = timeout;
+ }
+
+ const name = this.getName(options);
+ options._agentKey = name;
+
+ debug('createConnection', name);
+ options.encoding = null;
+
+ const oncreate = once((err, s) => {
+ if (err)
+ return cb(err);
+ this.sockets[name] ||= [];
+ this.sockets[name].push(s);
+ this.totalSocketCount++;
+ debug('sockets', name, this.sockets[name].length, this.totalSocketCount);
+ installListeners(this, s, options);
+ cb(null, s);
+ });
+ // When keepAlive is true, pass the related options to createConnection
+ if (this.keepAlive) {
+ options.keepAlive = this.keepAlive;
+ options.keepAliveInitialDelay = this.keepAliveMsecs;
+ }
+
+ const newSocket = this.createConnection(options, oncreate);
+ // In the case where we are proxying through a tunnel for HTTPS, only add
+ // the socket to the pool and install/invoke the listeners after
+ // the tunnel is successfully established, so that actual operations
+ // on the socket all go through the tunnel. Errors emitted during
+ // tunnel establishment will be handled in the createConnection method
+ // in lib/https.js.
+ if (newSocket && !newSocket[kWaitForProxyTunnel])
+ oncreate(null, newSocket);
+};
+
+function normalizeServerName(options, req) {
+ if (!options.servername && options.servername !== '')
+ options.servername = calculateServerName(options, req);
+}
+
+function calculateServerName(options, req) {
+ let servername = options.host;
+ const hostHeader = req.getHeader('host');
+ if (hostHeader) {
+ validateString(hostHeader, 'options.headers.host');
+
+ // abc => abc
+ // abc:123 => abc
+ // [::1] => ::1
+ // [::1]:123 => ::1
+ if (hostHeader[0] === '[') {
+ const index = hostHeader.indexOf(']');
+ if (index === -1) {
+ // Leading '[', but no ']'. Need to do something...
+ servername = hostHeader;
+ } else {
+ servername = hostHeader.substring(1, index);
+ }
+ } else {
+ servername = hostHeader.split(':', 1)[0];
+ }
+ }
+ // Don't implicitly set invalid (IP) servernames.
+ if (net.isIP(servername))
+ servername = '';
+ return servername;
+}
+
+function installListeners(agent, s, options) {
+ function onFree() {
+ debug('CLIENT socket onFree');
+ agent.emit('free', s, options);
+ }
+ s.on('free', onFree);
+
+ function onClose(err) {
+ debug('CLIENT socket onClose');
+ // This is the only place where sockets get removed from the Agent.
+ // If you want to remove a socket from the pool, just close it.
+ // All socket errors end in a close event anyway.
+ agent.totalSocketCount--;
+ agent.removeSocket(s, options);
+ }
+ s.on('close', onClose);
+
+ function onTimeout() {
+ debug('CLIENT socket onTimeout');
+
+ // Destroy if in free list.
+ // TODO(ronag): Always destroy, even if not in free list.
+ const sockets = agent.freeSockets;
+ if (ObjectKeys(sockets).some((name) => sockets[name].includes(s))) {
+ return s.destroy();
+ }
+ }
+ s.on('timeout', onTimeout);
+
+ function onRemove() {
+ // We need this function for cases like HTTP 'upgrade'
+ // (defined by WebSockets) where we need to remove a socket from the
+ // pool because it'll be locked up indefinitely
+ debug('CLIENT socket onRemove');
+ agent.totalSocketCount--;
+ agent.removeSocket(s, options);
+ s.removeListener('close', onClose);
+ s.removeListener('free', onFree);
+ s.removeListener('timeout', onTimeout);
+ s.removeListener('agentRemove', onRemove);
+ }
+ s.on('agentRemove', onRemove);
+
+ if (agent[kOnKeylog]) {
+ s.on('keylog', agent[kOnKeylog]);
+ }
+}
+
+Agent.prototype.removeSocket = function removeSocket(s, options) {
+ const name = this.getName(options);
+ debug('removeSocket', name, 'writable:', s.writable);
+ const sets = [this.sockets];
+
+ // If the socket was destroyed, remove it from the free buffers too.
+ if (!s.writable)
+ sets.push(this.freeSockets);
+
+ for (let sk = 0; sk < sets.length; sk++) {
+ const sockets = sets[sk];
+
+ if (sockets[name]) {
+ const index = sockets[name].indexOf(s);
+ if (index !== -1) {
+ sockets[name].splice(index, 1);
+ // Don't leak
+ if (sockets[name].length === 0)
+ delete sockets[name];
+ }
+ }
+ }
+
+ let req;
+ if (this.requests[name]?.length) {
+ debug('removeSocket, have a request, make a socket');
+ req = this.requests[name][0];
+ } else {
+ // TODO(rickyes): this logic will not be FIFO across origins.
+ // There might be older requests in a different origin, but
+ // if the origin which releases the socket has pending requests
+ // that will be prioritized.
+ const keys = ObjectKeys(this.requests);
+ for (let i = 0; i < keys.length; i++) {
+ const prop = keys[i];
+ // Check whether this specific origin is already at maxSockets
+ if (this.sockets[prop]?.length) break;
+ debug('removeSocket, have a request with different origin,' +
+ ' make a socket');
+ req = this.requests[prop][0];
+ options = req[kRequestOptions];
+ break;
+ }
+ }
+
+ if (req && options) {
+ req[kRequestOptions] = undefined;
+ // If we have pending requests and a socket gets closed make a new one
+ this.createSocket(req, options, (err, socket) => {
+ if (err) {
+ handleSocketAfterProxy(err, req);
+ req.onSocket(null, err);
+ return;
+ }
+
+ socket.emit('free');
+ });
+ }
+
+};
+
+Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
+ socket.setKeepAlive(true, this.keepAliveMsecs);
+ socket.unref();
+
+ let agentTimeout = this.options.timeout || 0;
+ let canKeepSocketAlive = true;
+
+ if (socket._httpMessage?.res) {
+ const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
+
+ if (keepAliveHint) {
+ const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];
+
+ if (hint) {
+ // Let the timer expire before the announced timeout to reduce
+ // the likelihood of ECONNRESET errors
+ let serverHintTimeout = (NumberParseInt(hint) * 1000) - this.agentKeepAliveTimeoutBuffer;
+ serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
+ if (serverHintTimeout === 0) {
+ // Cannot safely reuse the socket because the server timeout is
+ // too short
+ canKeepSocketAlive = false;
+ } else if (serverHintTimeout < agentTimeout) {
+ agentTimeout = serverHintTimeout;
+ }
+ }
+ }
+ }
+
+ if (socket.timeout !== agentTimeout) {
+ socket.setTimeout(agentTimeout);
+ }
+
+ return canKeepSocketAlive;
+};
+
+Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
+ debug('have free socket');
+ socket.removeListener('error', freeSocketErrorListener);
+ removeFreeSocketDataGuard(socket);
+ req.reusedSocket = true;
+ socket.ref();
+};
+
+Agent.prototype.destroy = function destroy() {
+ const sets = [this.freeSockets, this.sockets];
+ for (let s = 0; s < sets.length; s++) {
+ const set = sets[s];
+ const keys = ObjectKeys(set);
+ for (let v = 0; v < keys.length; v++) {
+ const setName = set[keys[v]];
+ for (let n = 0; n < setName.length; n++) {
+ setName[n].destroy();
+ }
+ }
+ }
+};
+
+function setRequestSocket(agent, req, socket) {
+ req.onSocket(socket);
+ const agentTimeout = agent.options.timeout || 0;
+ if (req.timeout === undefined || req.timeout === agentTimeout) {
+ return;
+ }
+ socket.setTimeout(req.timeout);
+}
+
+function asyncResetHandle(socket) {
+ // Guard against an uninitialized or user supplied Socket.
+ const handle = socket._handle;
+ if (handle && typeof handle.asyncReset === 'function') {
+ // Assign the handle a new asyncId and run any destroy()/init() hooks.
+ handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle));
+ socket[async_id_symbol] = handle.getAsyncId();
+ }
+}
+
+module.exports = {
+ Agent,
+ globalAgent: getGlobalAgent(getOptionValue('--use-env-proxy') ? process.env : undefined, Agent),
+};
diff --git a/lib/internal/http/client.js b/lib/internal/http/client.js
new file mode 100644
index 00000000000..1952e7b69af
--- /dev/null
+++ b/lib/internal/http/client.js
@@ -0,0 +1,1303 @@
+// 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 {
+ ArrayIsArray,
+ Boolean,
+ Error,
+ NumberIsFinite,
+ ObjectAssign,
+ ObjectDefineProperty,
+ ObjectHasOwn,
+ ObjectKeys,
+ ObjectSetPrototypeOf,
+ ReflectApply,
+ String,
+ Symbol,
+} = primordials;
+
+const net = require('net');
+const assert = require('internal/assert');
+const {
+ deprecateInstantiation,
+ kEmptyObject,
+ once,
+} = require('internal/util');
+const {
+ _checkIsHttpToken: checkIsHttpToken,
+ freeParser,
+ parsers,
+ HTTPParser,
+ calculateLenientFlags,
+ prepareError,
+ kSkipPendingData,
+} = require('internal/http/common');
+const { kDetachAbortSignal } = require('internal/http/incoming');
+const {
+ kHighWaterMark,
+ kUniqueHeaders,
+ parseUniqueHeadersOption,
+ OutgoingMessage,
+} = require('internal/http/outgoing');
+const Agent = require('internal/http/agent');
+const { Buffer } = require('buffer');
+const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
+const { URL, urlToHttpOptions, isURL } = require('internal/url');
+const {
+ kOutHeaders,
+ kNeedDrain,
+ isTraceHTTPEnabled,
+ traceBegin,
+ traceEnd,
+ getNextTraceEventId,
+ kProxyConfig,
+ checkShouldUseProxy,
+} = require('internal/http');
+const {
+ ConnResetException,
+ codes: {
+ ERR_HTTP_HEADERS_SENT,
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_INVALID_HTTP_TOKEN,
+ ERR_INVALID_PROTOCOL,
+ ERR_UNESCAPED_CHARACTERS,
+ },
+} = require('internal/errors');
+const {
+ validateInteger,
+ validateBoolean,
+ validateOneOf,
+ validatePort,
+ validateString,
+} = require('internal/validators');
+const { getTimerDuration } = require('internal/timers');
+const {
+ hasObserver,
+ startPerf,
+ stopPerf,
+} = require('internal/perf/observe');
+
+const kClientRequestStatistics = Symbol('ClientRequestStatistics');
+
+const dc = require('diagnostics_channel');
+const onClientRequestCreatedChannel = dc.channel('http.client.request.created');
+const onClientRequestStartChannel = dc.channel('http.client.request.start');
+const onClientRequestErrorChannel = dc.channel('http.client.request.error');
+const onClientResponseFinishChannel = dc.channel('http.client.response.finish');
+
+function emitErrorEvent(request, error) {
+ if (onClientRequestErrorChannel.hasSubscribers) {
+ onClientRequestErrorChannel.publish({
+ request,
+ error,
+ });
+ }
+ request.emit('error', error);
+}
+
+const { addAbortSignal, finished } = require('stream');
+
+let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
+ debug = fn;
+});
+
+const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
+const kError = Symbol('kError');
+const kPath = Symbol('kPath');
+const kAuthority = Symbol('kAuthority');
+const kProxyRewrittenToAbsolute = Symbol('kProxyRewrittenToAbsolute');
+
+const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request';
+
+function validateHost(host, name) {
+ if (host !== null && host !== undefined && typeof host !== 'string') {
+ throw new ERR_INVALID_ARG_TYPE(`options.${name}`,
+ ['string', 'undefined', 'null'],
+ host);
+ }
+ return host;
+}
+
+class HTTPClientAsyncResource {
+ constructor(type, req) {
+ this.type = type;
+ this.req = req;
+ }
+}
+
+// The only documented shape is [k, v, k, v, ...]. Here we also accept [[k, v], [k, v], ...].
+// for backward compatibility, and reject others. Also reject if there are duplicate Host entries.
+// Returns the Host header value, or undefined if absent.
+function getHostFromHeaderArray(headers) {
+ let host;
+ const isPairs = headers.length > 0 && ArrayIsArray(headers[0]);
+ if (isPairs) {
+ for (let i = 0; i < headers.length; i++) {
+ const entry = headers[i];
+ if (!ArrayIsArray(entry)) {
+ throw new ERR_INVALID_ARG_VALUE(`options.headers[${i}]`, typeof entry,
+ 'must be an array when headers is passed as an array of pairs');
+ }
+ if (`${entry[0]}`.toLowerCase() === 'host') {
+ if (host !== undefined) {
+ throw new ERR_INVALID_ARG_VALUE('options.headers', '(redacted)',
+ 'must not contain duplicate Host headers');
+ }
+ host = `${entry[1]}`;
+ }
+ }
+ } else {
+ for (let i = 0; i + 1 < headers.length; i += 2) {
+ if (`${headers[i]}`.toLowerCase() === 'host') {
+ if (host !== undefined) {
+ throw new ERR_INVALID_ARG_VALUE('options.headers', '(redacted)',
+ 'must not contain duplicate Host headers');
+ }
+ host = `${headers[i + 1]}`;
+ }
+ }
+ }
+ return host;
+}
+
+function authoritiesMatch(canonicalHost, hostFromHeader) {
+ let parsed;
+ try {
+ parsed = new URL(`http://${hostFromHeader}`);
+ } catch {
+ return false;
+ }
+ if (parsed.username || parsed.password ||
+ parsed.pathname !== '/' || parsed.search || parsed.hash) {
+ return false;
+ }
+ return parsed.host === canonicalHost;
+}
+
+// https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
+// When the request target is in absolute-form, ensure it is consistent with
+// the request authority: same scheme, no userinfo, and an authority
+// component agree with options.host[:port].
+function validateRequestAuthority(pathOption, proxyAuthority, userHostHeader, headerArray) {
+ validatePort(proxyAuthority.port, 'options.port', true);
+ pathOption = `${pathOption}`;
+ const requestBase = new URL(`http://${proxyAuthority.host}`);
+ requestBase.port = proxyAuthority.port;
+
+ const result = { requestBase };
+ if (headerArray !== undefined) {
+ const host = getHostFromHeaderArray(headerArray);
+ // Since we don't mutate the header array to normalize the Host value, unlike
+ // in the case of other shapes of headers provided, we check that it is identical
+ // to the authority from the requestBase.
+ if (host !== undefined && host !== requestBase.host) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'Host in options.headers', host,
+ `must match the request authority (${requestBase.host})`);
+ }
+ } else if (userHostHeader !== undefined) {
+ if (!authoritiesMatch(requestBase.host, userHostHeader)) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'Host in options.headers', userHostHeader,
+ `must match the request authority (${requestBase.host})`);
+ }
+ }
+
+ // Per RFC 9112 Section 3.2, if request target is in absolute-form its authority
+ // must agree with the request authority.
+ let requestURL;
+ let isAbsoluteForm = false;
+ try {
+ requestURL = new URL(pathOption);
+ isAbsoluteForm = true;
+ } catch {
+ if (pathOption.charCodeAt(0) !== 0x2F) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.path', pathOption, 'must be in absolute-form or start with /');
+ }
+ requestURL = new URL(requestBase.origin + pathOption);
+ }
+ result.requestURL = requestURL;
+ if (!isAbsoluteForm) {
+ return result;
+ }
+
+ if (requestURL.username || requestURL.password) {
+ requestURL.username = '';
+ requestURL.password = '';
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.path', requestURL.href, 'must not contain userinfo, use options.auth instead');
+ }
+
+ if (requestURL.protocol !== 'http:') {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.path', requestURL.protocol, 'must use http: scheme when specified as an absolute URL');
+ }
+
+ if (requestBase.host !== requestURL.host) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.path', requestURL, `must match the request authority (${requestBase.host})`);
+ }
+
+ return result;
+}
+
+// When proxying a HTTP request, the following needs to be done:
+// https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.2
+// 1. Rewrite the request path to absolute-form.
+// 2. Add proxy-connection and proxy-authorization headers appropriately.
+//
+// This function checks whether the request should be rewritten for proxying
+// and modifies the headers as well as req.path if necessary.
+// The handling of the proxy server connection is done in createConnection.
+// It also validates that the Host header and absolute-form path authority match the
+// request authority specified by reqOptions.
+function rewriteForProxiedHttp(req, reqOptions, proxyAuthority, userHostHeader, headerArray) {
+ if (req._header) {
+ debug('request._header is already sent, skipping rewriteForProxiedHttp', reqOptions);
+ return false;
+ }
+ const agent = req.agent;
+ if (!agent || !agent[kProxyConfig]) {
+ return false;
+ }
+ if ((reqOptions.protocol || agent.protocol) !== 'http:') {
+ return false;
+ }
+ // TODO(joyeecheung): cache this on the req or on the agent.
+ const shouldUseProxy = checkShouldUseProxy(agent[kProxyConfig], reqOptions);
+ debug(`rewriteForProxiedHttp should use proxy for ${reqOptions.host}:${reqOptions.port}:`, shouldUseProxy);
+ if (!shouldUseProxy) {
+ return false;
+ }
+
+ // Per RFC 9112 Section 3.2.2, we don't need to rewrite CONNECT or OPTIONS * requests.
+ let requestURL;
+ if (req.method !== 'CONNECT' && !(req.method === 'OPTIONS' && req.path === '*')) {
+ // Validate Host header values agree with the request authority before mutating req,
+ // so a rejected request doesn't leave proxy-* headers stuck on the outgoing header store.
+ // XXX(joyeecheung): This validates whether the request conforms to the RFC, but here
+ // we only do it for proxied requests for backward compatibility. For non-proxied requests,
+ // ensuring that the request is well formed has been entirely left to the user.
+ const result = validateRequestAuthority(req.path, proxyAuthority, userHostHeader, headerArray);
+ if (headerArray === undefined) {
+ const currentHost = req.getHeader('host');
+ if (currentHost !== undefined && currentHost !== result.requestBase.host) {
+ req.setHeader('Host', result.requestBase.host);
+ }
+ }
+ requestURL = result.requestURL;
+ }
+
+ // Add proxy headers.
+ const { auth, href } = agent[kProxyConfig];
+ if (auth) {
+ req.setHeader('proxy-authorization', auth);
+ }
+ if (req.shouldKeepAlive) {
+ req.setHeader('proxy-connection', 'keep-alive');
+ } else {
+ req.setHeader('proxy-connection', 'close');
+ }
+
+ if (requestURL !== undefined) {
+ // Convert the path to absolute-form. The authority is built from options.
+ req.path = requestURL.href;
+ req[kProxyRewrittenToAbsolute] = true;
+ }
+ debug(`updated request for HTTP proxy ${href} with ${req.path} `, req[kOutHeaders]);
+ return true;
+};
+
+function ClientRequest(input, options, cb) {
+ if (!(this instanceof ClientRequest)) {
+ return deprecateInstantiation(ClientRequest, 'DEP0195', input, options, cb);
+ }
+
+ OutgoingMessage.call(this);
+
+ let pathIsFromURL = false;
+ if (typeof input === 'string') {
+ const urlStr = input;
+ input = urlToHttpOptions(new URL(urlStr));
+ pathIsFromURL = true;
+ } else if (isURL(input)) {
+ // url.URL instance
+ input = urlToHttpOptions(input);
+ pathIsFromURL = true;
+ } else {
+ cb = options;
+ options = input;
+ input = null;
+ }
+
+ if (typeof options === 'function') {
+ cb = options;
+ options = input || kEmptyObject;
+ } else {
+ const hasPathOverride = pathIsFromURL &&
+ options != null &&
+ ObjectHasOwn(options, 'path');
+ if (hasPathOverride) {
+ pathIsFromURL = false;
+ }
+
+ options = ObjectAssign({ __proto__: null }, input, options);
+ }
+
+ // Propagate the user's highWaterMark to OutgoingMessage so that
+ // _writeRaw() uses the correct threshold for writes buffered before
+ // the socket connects (Path B). Without this, the OutgoingMessage
+ // defaults to 64 KB regardless of what the caller requested.
+ if (options.highWaterMark != null) {
+ this[kHighWaterMark] = options.highWaterMark;
+ }
+
+ let agent = options.agent;
+ const defaultAgent = options._defaultAgent || Agent.globalAgent;
+ if (agent === false) {
+ agent = new defaultAgent.constructor();
+ } else if (agent === null || agent === undefined) {
+ if (typeof options.createConnection !== 'function') {
+ agent = defaultAgent;
+ }
+ // Explicitly pass through this statement as agent will not be used
+ // when createConnection is provided.
+ } else if (typeof agent.addRequest !== 'function') {
+ throw new ERR_INVALID_ARG_TYPE('options.agent',
+ ['Agent-like Object', 'undefined', 'false'],
+ agent);
+ }
+ this.agent = agent;
+
+ const protocol = options.protocol || defaultAgent.protocol;
+ let expectedProtocol = defaultAgent.protocol;
+ if (this.agent?.protocol)
+ expectedProtocol = this.agent.protocol;
+
+ if (options.path) {
+ const path = String(options.path);
+ if (INVALID_PATH_REGEX.test(path)) {
+ debug('Path contains unescaped characters: "%s"', path);
+ throw new ERR_UNESCAPED_CHARACTERS('Request path');
+ }
+ }
+
+ if (protocol !== expectedProtocol) {
+ throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol);
+ }
+
+ const defaultPort = options.defaultPort ||
+ (this.agent?.defaultPort);
+
+ const optsWithoutSignal = { __proto__: null, ...options };
+
+ const port = optsWithoutSignal.port = options.port || defaultPort || 80;
+ const host = optsWithoutSignal.host = validateHost(options.hostname, 'hostname') ||
+ validateHost(options.host, 'host') || 'localhost';
+
+ const setHost = options.setHost !== undefined ?
+ Boolean(options.setHost) :
+ options.setDefaultHeaders !== false;
+
+ this._removedConnection = options.setDefaultHeaders === false;
+ this._removedContLen = options.setDefaultHeaders === false;
+ this._removedTE = options.setDefaultHeaders === false;
+
+ this.socketPath = options.socketPath;
+
+ if (options.timeout !== undefined)
+ this.timeout = getTimerDuration(options.timeout, 'timeout');
+
+ const signal = options.signal;
+ if (signal) {
+ addAbortSignal(signal, this);
+ delete optsWithoutSignal.signal;
+ }
+ let method = options.method;
+ if (method != null) {
+ validateString(method, 'options.method');
+ }
+
+ if (method) {
+ if (!checkIsHttpToken(method)) {
+ throw new ERR_INVALID_HTTP_TOKEN('Method', method);
+ }
+ method = this.method = method.toUpperCase();
+ } else {
+ method = this.method = 'GET';
+ }
+
+ const maxHeaderSize = options.maxHeaderSize;
+ if (maxHeaderSize !== undefined)
+ validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
+ this.maxHeaderSize = maxHeaderSize;
+
+ const insecureHTTPParser = options.insecureHTTPParser;
+ if (insecureHTTPParser !== undefined) {
+ validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
+ }
+
+ this.insecureHTTPParser = insecureHTTPParser;
+
+ const httpValidation = options.httpValidation;
+ if (httpValidation !== undefined) {
+ validateOneOf(httpValidation, 'options.httpValidation',
+ ['strict', 'relaxed', 'insecure']);
+ if (insecureHTTPParser !== undefined) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.httpValidation',
+ httpValidation,
+ 'cannot be used together with options.insecureHTTPParser',
+ );
+ }
+ }
+
+ this.httpValidation = httpValidation;
+
+ if (options.joinDuplicateHeaders !== undefined) {
+ validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders');
+ }
+
+ this.joinDuplicateHeaders = options.joinDuplicateHeaders;
+
+ let path = options.path || '/';
+ // Strip the leading slash added when the CONNECT target comes from a URL.
+ if (method === 'CONNECT' && pathIsFromURL && path[0] === '/') {
+ path = path.slice(1) || '/';
+ }
+
+ this[kPath] = path;
+ if (cb) {
+ this.once('response', cb);
+ }
+
+ if (method === 'GET' ||
+ method === 'HEAD' ||
+ method === 'DELETE' ||
+ method === 'OPTIONS' ||
+ method === 'TRACE' ||
+ method === 'CONNECT') {
+ this.useChunkedEncodingByDefault = false;
+ } else {
+ this.useChunkedEncodingByDefault = true;
+ }
+
+ this._ended = false;
+ this.res = null;
+ this.aborted = false;
+ this.timeoutCb = null;
+ this.upgradeOrConnect = false;
+ this.parser = null;
+ this.maxHeadersCount = null;
+ this.reusedSocket = false;
+ this.host = host;
+ this.protocol = protocol;
+ this[kProxyRewrittenToAbsolute] = false;
+
+ if (this.agent) {
+ // If there is an agent we should default to Connection:keep-alive,
+ // but only if the Agent will actually reuse the connection!
+ // If it's not a keepAlive agent, and the maxSockets==Infinity, then
+ // there's never a case where this socket will actually be reused
+ if (!this.agent.keepAlive && !NumberIsFinite(this.agent.maxSockets)) {
+ this._last = true;
+ this.shouldKeepAlive = false;
+ } else {
+ this._last = false;
+ this.shouldKeepAlive = true;
+ }
+ }
+
+ let hostHeaderFromOptions = host;
+ // For the Host header, ensure that IPv6 addresses are enclosed
+ // in square brackets, as defined by URI formatting
+ // https://tools.ietf.org/html/rfc3986#section-3.2.2
+ const posColon = hostHeaderFromOptions.indexOf(':');
+ if (posColon !== -1 &&
+ hostHeaderFromOptions.includes(':', posColon + 1) &&
+ hostHeaderFromOptions.charCodeAt(0) !== 91/* '[' */) {
+ hostHeaderFromOptions = `[${hostHeaderFromOptions}]`;
+ }
+ const proxyAuthority = { host: hostHeaderFromOptions, port };
+
+ if (port && +port !== defaultPort) {
+ hostHeaderFromOptions += ':' + port;
+ }
+ // Preserve the request authority (with the port when non-default) so that
+ // the perf_hooks entry can report a faithful URL.
+ this[kAuthority] = hostHeaderFromOptions;
+ const headersArray = ArrayIsArray(options.headers);
+ if (!headersArray) {
+ if (options.headers) {
+ const keys = ObjectKeys(options.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];
+ this.setHeader(key, options.headers[key]);
+ }
+ }
+
+ // Save the Host header before the implicit auto-set below, so the
+ // proxy validator can tell user-explicit values from Node-generated ones.
+ const userHostHeader = this.getHeader('host');
+
+ if (host && !this.getHeader('host') && setHost) {
+ const hostHeader = method === 'CONNECT' && options.path ?
+ String(this.path) : hostHeaderFromOptions;
+ this.setHeader('Host', hostHeader);
+ }
+
+ if (options.auth && !this.getHeader('Authorization')) {
+ this.setHeader('Authorization', 'Basic ' +
+ Buffer.from(options.auth).toString('base64'));
+ }
+
+ if (this.getHeader('expect')) {
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT('render');
+ }
+
+ rewriteForProxiedHttp(this, optsWithoutSignal, proxyAuthority, userHostHeader);
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
+ this[kOutHeaders]);
+ } else {
+ rewriteForProxiedHttp(this, optsWithoutSignal, proxyAuthority, userHostHeader);
+ }
+ } else {
+ rewriteForProxiedHttp(this, optsWithoutSignal, proxyAuthority, undefined, options.headers);
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
+ options.headers);
+ }
+
+ this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
+
+ // initiate connection
+ if (this.agent) {
+ this.agent.addRequest(this, optsWithoutSignal);
+ } else {
+ // No agent, default to Connection:close.
+ this._last = true;
+ this.shouldKeepAlive = false;
+ let opts = optsWithoutSignal;
+ if (opts.path || opts.socketPath) {
+ opts = { ...optsWithoutSignal };
+ if (opts.socketPath) {
+ opts.path = opts.socketPath;
+ } else {
+ opts.path &&= undefined;
+ }
+ }
+ if (typeof opts.createConnection === 'function') {
+ const oncreate = once((err, socket) => {
+ if (err) {
+ process.nextTick(() => emitErrorEvent(this, err));
+ } else {
+ this.onSocket(socket);
+ }
+ });
+
+ try {
+ const newSocket = opts.createConnection(opts, oncreate);
+ if (newSocket) {
+ oncreate(null, newSocket);
+ }
+ } catch (err) {
+ oncreate(err);
+ }
+ } else {
+ debug('CLIENT use net.createConnection', opts);
+ this.onSocket(net.createConnection(opts));
+ }
+ }
+ if (onClientRequestCreatedChannel.hasSubscribers) {
+ onClientRequestCreatedChannel.publish({
+ request: this,
+ });
+ }
+}
+ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype);
+ObjectSetPrototypeOf(ClientRequest, OutgoingMessage);
+
+ObjectDefineProperty(ClientRequest.prototype, 'path', {
+ __proto__: null,
+ get() {
+ return this[kPath];
+ },
+ set(value) {
+ const path = String(value);
+ if (INVALID_PATH_REGEX.test(path)) {
+ throw new ERR_UNESCAPED_CHARACTERS('Request path');
+ }
+ this[kPath] = path;
+ },
+ configurable: true,
+ enumerable: true,
+});
+
+ClientRequest.prototype._finish = function _finish() {
+ OutgoingMessage.prototype._finish.call(this);
+ if (hasObserver('http')) {
+ startPerf(this, kClientRequestStatistics, {
+ type: 'http',
+ name: 'HttpClient',
+ detail: {
+ req: {
+ method: this.method,
+ // If the path has been rewritten to absolute-form for proxying,
+ // it is already a full URL.
+ url: this[kProxyRewrittenToAbsolute] ?
+ this.path :
+ `${this.protocol}//${this[kAuthority]}${this.path}`,
+ headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {},
+ },
+ },
+ });
+ }
+ if (onClientRequestStartChannel.hasSubscribers) {
+ onClientRequestStartChannel.publish({
+ request: this,
+ });
+ }
+ if (isTraceHTTPEnabled()) {
+ this._traceEventId = getNextTraceEventId();
+ traceBegin(HTTP_CLIENT_TRACE_EVENT_NAME, this._traceEventId);
+ }
+};
+
+ClientRequest.prototype._implicitHeader = function _implicitHeader() {
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT('render');
+ }
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
+ this[kOutHeaders]);
+};
+
+ClientRequest.prototype.abort = function abort() {
+ if (this.aborted) {
+ return;
+ }
+ this.aborted = true;
+ process.nextTick(emitAbortNT, this);
+ this.destroy();
+};
+
+ClientRequest.prototype.destroy = function destroy(err) {
+ if (this.destroyed) {
+ return this;
+ }
+ this.destroyed = true;
+
+ // If we're aborting, we don't care about any more response data.
+ if (this.res) {
+ this.res._dump();
+ }
+
+ // Nothing left to cancel: the request was fully sent and the response was
+ // fully received. Destroying the socket here would emit an error on a
+ // socket that is already being released to the agent, at which point
+ // socketErrorListener has been removed and nothing would handle it.
+ if (this.writableFinished && this.res?.complete) {
+ return this;
+ }
+
+ this[kError] = err;
+ this.socket?.destroy(err);
+
+ return this;
+};
+
+function emitAbortNT(req) {
+ req.emit('abort');
+}
+
+function ondrain() {
+ const msg = this._httpMessage;
+ if (msg && !msg.finished && msg[kNeedDrain]) {
+ msg[kNeedDrain] = false;
+ msg.emit('drain');
+ }
+}
+
+function socketCloseListener() {
+ const socket = this;
+ const req = socket._httpMessage;
+ debug('HTTP socket close');
+
+ // NOTE: It's important to get parser here, because it could be freed by
+ // the `socketOnData`.
+ const parser = socket.parser;
+ const res = req.res;
+
+ req.destroyed = true;
+ if (res) {
+ // Socket closed before we emitted 'end' below.
+ if (!res.complete) {
+ res.destroy(new ConnResetException('aborted',
+ req[kError] ? { cause: req[kError] } : undefined));
+ }
+ req._closed = true;
+ req.emit('close');
+ if (!res.aborted && res.readable) {
+ res.push(null);
+ }
+ } else {
+ if (!req.socket._hadError) {
+ // This socket error fired before we started to
+ // receive a response. The error needs to
+ // fire on the request.
+ req.socket._hadError = true;
+ emitErrorEvent(req, new ConnResetException('socket hang up'));
+ }
+ req._closed = true;
+ req.emit('close');
+ }
+
+ // Too bad. That output wasn't getting written.
+ // This is pretty terrible that it doesn't raise an error.
+ // Fixed better in v0.10
+ if (req.outputData)
+ req.outputData.length = 0;
+
+ if (parser) {
+ parser.finish();
+ freeParser(parser, req, socket);
+ }
+}
+
+function socketErrorListener(err) {
+ const socket = this;
+ const req = socket._httpMessage;
+ debug('SOCKET ERROR:', err.message, err.stack);
+
+ if (req) {
+ // For Safety. Some additional errors might fire later on
+ // and we need to make sure we don't double-fire the error event.
+ socket._hadError = true;
+ emitErrorEvent(req, err);
+ // Preserve the socket error for an incomplete response, including TLS
+ // errors that are emitted without setting socket.errored.
+ req[kError] ||= err;
+ }
+
+ const parser = socket.parser;
+ if (parser) {
+ parser.finish();
+ freeParser(parser, req, socket);
+ }
+
+ // Ensure that no further data will come out of the socket
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+ socket.destroy();
+}
+
+function socketOnEnd() {
+ const socket = this;
+ const req = this._httpMessage;
+ const parser = this.parser;
+
+ if (!req.res && !req.socket._hadError) {
+ // If we don't have a response then we know that the socket
+ // ended prematurely and we need to emit an error on the request.
+ req.socket._hadError = true;
+ emitErrorEvent(req, new ConnResetException('socket hang up'));
+ }
+ if (parser) {
+ parser.finish();
+ freeParser(parser, req, socket);
+ }
+ socket.destroy();
+}
+
+function socketOnData(d) {
+ const socket = this;
+ const req = this._httpMessage;
+ const parser = this.parser;
+
+ assert(parser && parser.socket === socket);
+
+ const ret = parser.execute(d);
+ if (ret instanceof Error) {
+ prepareError(ret, parser, d);
+ debug('parse error', ret);
+ freeParser(parser, req, socket);
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+ socket.destroy();
+ req.socket._hadError = true;
+ emitErrorEvent(req, ret);
+ } else if (parser.incoming?.upgrade) {
+ // Upgrade (if status code 101) or CONNECT
+ const bytesParsed = ret;
+ const res = parser.incoming;
+ req.res = res;
+
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+ socket.removeListener('drain', ondrain);
+
+ if (req.timeoutCb) socket.removeListener('timeout', req.timeoutCb);
+ socket.removeListener('timeout', responseOnTimeout);
+
+ parser.finish();
+ freeParser(parser, req, socket);
+
+ const bodyHead = d.slice(bytesParsed, d.length);
+
+ const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
+ if (req.listenerCount(eventName) > 0) {
+ req.upgradeOrConnect = true;
+
+ // detach the socket
+ socket.emit('agentRemove');
+ socket.removeListener('close', socketCloseListener);
+ socket.removeListener('error', socketErrorListener);
+
+ socket._httpMessage = null;
+ socket.readableFlowing = null;
+
+ req.emit(eventName, res, socket, bodyHead);
+ req.destroyed = true;
+ req._closed = true;
+ req.emit('close');
+ } else {
+ // Requested Upgrade or used CONNECT method, but have no handler.
+ socket.destroy();
+ }
+ } else if (parser.incoming?.complete &&
+ // When the status code is informational (100, 102-199),
+ // the server will send a final response after this client
+ // sends a request body, so we must not free the parser.
+ // 101 (Switching Protocols) and all other status codes
+ // should be processed normally.
+ !statusIsInformational(parser.incoming.statusCode)) {
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+ socket.removeListener('drain', ondrain);
+ freeParser(parser, req, socket);
+ }
+}
+
+function statusIsInformational(status) {
+ // 100 (Continue) RFC7231 Section 6.2.1
+ // 102 (Processing) RFC2518
+ // 103 (Early Hints) RFC8297
+ // 104-199 (Unassigned)
+ return (status < 200 && status >= 100 && status !== 101);
+}
+
+// client
+function parserOnIncomingClient(res, shouldKeepAlive) {
+ const socket = this.socket;
+ const req = socket._httpMessage;
+
+ debug('AGENT incoming response!');
+
+ if (req.res) {
+ // We already have a response object, this means the server
+ // sent a double response.
+ socket.destroy();
+ if (socket.parser) {
+ // https://github.com/nodejs/node/issues/60025
+ // Now, parser.incoming is pointed to the new IncomingMessage,
+ // we need to rewrite it to the first one and skip all the pending IncomingMessage
+ socket.parser.incoming = req.res;
+ socket.parser.incoming[kSkipPendingData] = true;
+ }
+ return 0;
+ }
+ req.res = res;
+
+ // Skip body and treat as Upgrade.
+ if (res.upgrade)
+ return 2;
+
+ // Responses to CONNECT request is handled as Upgrade.
+ const method = req.method;
+ if (method === 'CONNECT') {
+ res.upgrade = true;
+ return 2; // Skip body and treat as Upgrade.
+ }
+
+ if (statusIsInformational(res.statusCode)) {
+ // Restart the parser, as this is a 1xx informational message.
+ req.res = null; // Clear res so that we don't hit double-responses.
+ // Maintain compatibility by sending 100-specific events
+ if (res.statusCode === 100) {
+ req.emit('continue');
+ }
+ // Send information events to all 1xx responses except 101 Upgrade.
+ req.emit('information', {
+ statusCode: res.statusCode,
+ statusMessage: res.statusMessage,
+ httpVersion: res.httpVersion,
+ httpVersionMajor: res.httpVersionMajor,
+ httpVersionMinor: res.httpVersionMinor,
+ headers: res.headers,
+ rawHeaders: res.rawHeaders,
+ });
+
+ return 1; // Skip body but don't treat as Upgrade.
+ }
+
+ if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) {
+ // Server MUST respond with Connection:keep-alive for us to enable it.
+ // If we've been upgraded (via WebSockets) we also shouldn't try to
+ // keep the connection open.
+ req.shouldKeepAlive = false;
+ }
+
+ if (req[kClientRequestStatistics] && hasObserver('http')) {
+ stopPerf(req, kClientRequestStatistics, {
+ detail: {
+ res: {
+ statusCode: res.statusCode,
+ statusMessage: res.statusMessage,
+ headers: res.headers,
+ },
+ },
+ });
+ }
+ if (onClientResponseFinishChannel.hasSubscribers) {
+ onClientResponseFinishChannel.publish({
+ request: req,
+ response: res,
+ });
+ }
+ if (isTraceHTTPEnabled() && typeof req._traceEventId === 'number') {
+ traceEnd(HTTP_CLIENT_TRACE_EVENT_NAME, req._traceEventId, {
+ path: req.path,
+ statusCode: res.statusCode,
+ });
+ }
+ req.res = res;
+ res.req = req;
+
+ // Add our listener first, so that we guarantee socket cleanup
+ res.on('end', responseOnEnd);
+ req.on('finish', requestOnFinish);
+ socket.on('timeout', responseOnTimeout);
+
+ // If the user did not listen for the 'response' event, then they
+ // can't possibly read the data, so we ._dump() it into the void
+ // so that the socket doesn't hang there in a paused state.
+ if (req.aborted || !req.emit('response', res))
+ res._dump();
+
+ if (method === 'HEAD')
+ return 1; // Skip body but don't treat as Upgrade.
+
+ if (res.statusCode === 304) {
+ res.complete = true;
+ return 1; // Skip body as there won't be any
+ }
+
+ return 0; // No special treatment.
+}
+
+// client
+function responseKeepAlive(req) {
+ const socket = req.socket;
+
+ debug('AGENT socket keep-alive');
+ if (req.timeoutCb) {
+ socket.setTimeout(0, req.timeoutCb);
+ req.timeoutCb = null;
+ }
+ socket.removeListener('close', socketCloseListener);
+ socket.removeListener('error', socketErrorListener);
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+
+ // TODO(ronag): Between here and emitFreeNT the socket
+ // has no 'error' handler.
+
+ // There are cases where _handle === null. Avoid those. Passing undefined to
+ // nextTick() will call getDefaultTriggerAsyncId() to retrieve the id.
+ const asyncId = socket._handle ? socket._handle.getAsyncId() : undefined;
+ // Mark this socket as available, AFTER user-added end
+ // handlers have a chance to run.
+ defaultTriggerAsyncIdScope(asyncId, process.nextTick, emitFreeNT, req);
+
+ req.destroyed = true;
+ if (req.res) {
+ // Detach socket from IncomingMessage to avoid destroying the freed
+ // socket in IncomingMessage.destroy().
+ req.res.socket = null;
+ }
+}
+
+function responseOnEnd() {
+ const req = this.req;
+ const socket = req.socket;
+
+ this[kDetachAbortSignal]();
+
+ if (socket) {
+ if (req.timeoutCb) socket.removeListener('timeout', emitRequestTimeout);
+ socket.removeListener('timeout', responseOnTimeout);
+ }
+
+ req._ended = true;
+
+ if (!req.shouldKeepAlive) {
+ if (socket.writable) {
+ debug('AGENT socket.destroySoon()');
+ if (typeof socket.destroySoon === 'function')
+ socket.destroySoon();
+ else
+ socket.end();
+ }
+ assert(!socket.writable);
+ } else if (req.writableFinished && !this.aborted) {
+ assert(req.finished);
+ // We can assume `req.finished` means all data has been written since:
+ // - `'responseOnEnd'` means we have been assigned a socket.
+ // - when we have a socket we write directly to it without buffering.
+ // - `req.finished` means `end()` has been called and no further data.
+ // can be written
+ // In addition, `req.writableFinished` means all data written has been
+ // accepted by the kernel. (i.e. the `req.socket` is drained).Without
+ // this constraint, we may assign a non drained socket to a request.
+ responseKeepAlive(req);
+ }
+}
+
+function responseOnTimeout() {
+ const req = this._httpMessage;
+ if (!req) return;
+ const res = req.res;
+ if (!res) return;
+ res.emit('timeout');
+}
+
+// This function is necessary in the case where we receive the entire response
+// from the server before we finish sending out the request.
+function requestOnFinish() {
+ const req = this;
+
+ // If the response ends before this request finishes writing, `responseOnEnd()`
+ // already released the socket. When `finish` fires later, that socket may
+ // belong to a different request, so only call `responseKeepAlive()` when the
+ // original request is still alive (`!req.destroyed`).
+ if (req.shouldKeepAlive && req._ended && !req.destroyed)
+ responseKeepAlive(req);
+}
+
+function emitFreeNT(req) {
+ req._closed = true;
+ req.emit('close');
+ if (req.socket) {
+ req.socket.emit('free');
+ }
+}
+
+function tickOnSocket(req, socket) {
+ const parser = parsers.alloc();
+ req.socket = socket;
+ const lenientFlags = calculateLenientFlags(req.httpValidation, req.insecureHTTPParser);
+ // Propagate headers limit from request object to parser
+ if (typeof req.maxHeadersCount === 'number') {
+ parser.maxHeaderPairs = req.maxHeadersCount << 1;
+ }
+ parser.initialize(HTTPParser.RESPONSE,
+ new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req),
+ req.maxHeaderSize || 0,
+ lenientFlags,
+ undefined,
+ parser.maxHeaderPairs);
+ parser.socket = socket;
+ parser.outgoing = req;
+ req.parser = parser;
+
+ socket.parser = parser;
+ socket._httpMessage = req;
+
+ parser.joinDuplicateHeaders = req.joinDuplicateHeaders;
+
+ parser.onIncoming = parserOnIncomingClient;
+ socket.on('data', socketOnData);
+ socket.on('end', socketOnEnd);
+ socket.on('close', socketCloseListener);
+ socket.on('drain', ondrain);
+
+ if (
+ req.timeout !== undefined ||
+ (req.agent?.options?.timeout)
+ ) {
+ listenSocketTimeout(req);
+ }
+ req.emit('socket', socket);
+}
+
+function emitRequestTimeout() {
+ const req = this._httpMessage;
+ if (req) {
+ req.emit('timeout');
+ }
+}
+
+function listenSocketTimeout(req) {
+ if (req.timeoutCb) {
+ return;
+ }
+ // Set timeoutCb so it will get cleaned up on request end.
+ req.timeoutCb = emitRequestTimeout;
+ // Delegate socket timeout event.
+ if (req.socket) {
+ req.socket.once('timeout', emitRequestTimeout);
+ } else {
+ req.on('socket', (socket) => {
+ socket.once('timeout', emitRequestTimeout);
+ });
+ }
+}
+
+ClientRequest.prototype.onSocket = function onSocket(socket, err) {
+ // Attach the error listener synchronously so that any errors emitted on
+ // the socket before onSocketNT runs (e.g. from a blocklist check or other
+ // next-tick error) are forwarded to the request and can be caught by the
+ // user's error handler. socketErrorListener requires socket._httpMessage
+ // to be set so we set it here too.
+ if (socket && !err) {
+ socket._httpMessage = this;
+ socket.on('error', socketErrorListener);
+ }
+ process.nextTick(onSocketNT, this, socket, err);
+};
+
+function onSocketNT(req, socket, err) {
+ if (req.destroyed || err) {
+ req.destroyed = true;
+
+ function _destroy(req, err) {
+ if (!req.aborted && !err) {
+ err = new ConnResetException('socket hang up');
+ }
+ // ERR_PROXY_TUNNEL is handled by the proxying logic.
+ // Skip if the error was already emitted by the early socketErrorListener.
+ if (err && err.code !== 'ERR_PROXY_TUNNEL' &&
+ !socket?._hadError) {
+ emitErrorEvent(req, err);
+ }
+ req._closed = true;
+ req.emit('close');
+ }
+
+ if (socket) {
+ if (!err && req.agent && !socket.destroyed) {
+ socket.emit('free');
+ socket.removeListener('error', socketErrorListener);
+ } else {
+ finished(socket.destroy(err || req[kError]), (er) => {
+ if (er?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
+ er = null;
+ }
+ _destroy(req, er || err);
+ });
+ return;
+ }
+ }
+
+ _destroy(req, err || req[kError]);
+ } else {
+ tickOnSocket(req, socket);
+ req._flush();
+ }
+}
+
+ClientRequest.prototype._deferToConnect = _deferToConnect;
+function _deferToConnect(method, arguments_) {
+ // This function is for calls that need to happen once the socket is
+ // assigned to this request and writable. It's an important promisy
+ // thing for all the socket calls that happen either now
+ // (when a socket is assigned) or in the future (when a socket gets
+ // assigned out of the pool and is eventually writable).
+
+ const callSocketMethod = () => {
+ if (method)
+ ReflectApply(this.socket[method], this.socket, arguments_);
+ };
+
+ const onSocket = () => {
+ if (this.socket.writable) {
+ callSocketMethod();
+ } else {
+ this.socket.once('connect', callSocketMethod);
+ }
+ };
+
+ if (!this.socket) {
+ this.once('socket', onSocket);
+ } else {
+ onSocket();
+ }
+}
+
+ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
+ if (this._ended) {
+ return this;
+ }
+
+ listenSocketTimeout(this);
+ msecs = getTimerDuration(msecs, 'msecs');
+ if (callback) this.once('timeout', callback);
+
+ if (this.socket) {
+ setSocketTimeout(this.socket, msecs);
+ } else {
+ this.once('socket', (sock) => setSocketTimeout(sock, msecs));
+ }
+
+ return this;
+};
+
+function setSocketTimeout(sock, msecs) {
+ if (sock.connecting) {
+ sock.once('connect', function() {
+ sock.setTimeout(msecs);
+ });
+ } else {
+ sock.setTimeout(msecs);
+ }
+}
+
+ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
+ this._deferToConnect('setNoDelay', [noDelay]);
+};
+
+ClientRequest.prototype.setSocketKeepAlive =
+ function setSocketKeepAlive(enable, initialDelay) {
+ this._deferToConnect('setKeepAlive', [enable, initialDelay]);
+ };
+
+ClientRequest.prototype.clearTimeout = function clearTimeout(cb) {
+ this.setTimeout(0, cb);
+};
+
+module.exports = {
+ ClientRequest,
+};
diff --git a/lib/internal/http/common.js b/lib/internal/http/common.js
new file mode 100644
index 00000000000..ed1defc49f4
--- /dev/null
+++ b/lib/internal/http/common.js
@@ -0,0 +1,345 @@
+// 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 {
+ MathMin,
+ Symbol,
+ Uint8Array,
+} = primordials;
+const { setImmediate } = require('timers');
+
+const { methods, allMethods, HTTPParser } = internalBinding('http_parser');
+const { getOptionValue } = require('internal/options');
+const insecureHTTPParser = getOptionValue('--insecure-http-parser');
+
+const FreeList = require('internal/freelist');
+const incoming = require('internal/http/incoming');
+const {
+ IncomingMessage,
+ readStart,
+ readStop,
+} = incoming;
+
+const kIncomingMessage = Symbol('IncomingMessage');
+const kSkipPendingData = Symbol('SkipPendingData');
+const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;
+const kOnHeaders = HTTPParser.kOnHeaders | 0;
+const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
+const kOnBody = HTTPParser.kOnBody | 0;
+const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
+const kOnExecute = HTTPParser.kOnExecute | 0;
+const kOnTimeout = HTTPParser.kOnTimeout | 0;
+
+const MAX_HEADER_PAIRS = 2000;
+
+// Only called in the slow case where slow means
+// that the request headers were either fragmented
+// across multiple TCP packets or too large to be
+// processed in a single run. This method is also
+// called to process trailing HTTP headers.
+function parserOnHeaders(headers, url) {
+ // Once we exceeded headers limit - stop collecting them
+ const capacity = this.maxHeaderPairs - this._headers.length;
+ if (this.maxHeaderPairs <= 0 || capacity >= headers.length) {
+ this._headers.push(...headers);
+ } else if (capacity > 0) {
+ this._headers.push(...headers.slice(0, capacity));
+ }
+ this._url += url;
+}
+
+const HTTP_VERSION_1_1 = '1.1';
+
+// `headers` and `url` are set only if .onHeaders() has not been called for
+// this request.
+// `url` is not set for response parsers but that's not applicable here since
+// all our parsers are request parsers.
+function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
+ url, statusCode, statusMessage, upgrade,
+ shouldKeepAlive) {
+ const parser = this;
+ const { socket } = parser;
+
+ if (headers === undefined) {
+ headers = parser._headers;
+ parser._headers = [];
+ }
+
+ if (url === undefined) {
+ url = parser._url;
+ parser._url = '';
+ }
+
+ // Parser is also used by http client
+ const ParserIncomingMessage = (socket?.server?.[kIncomingMessage]) ||
+ IncomingMessage;
+
+ const incoming = parser.incoming = new ParserIncomingMessage(socket);
+ incoming.httpVersionMajor = versionMajor;
+ incoming.httpVersionMinor = versionMinor;
+ incoming.httpVersion = versionMajor === 1 && versionMinor === 1 ?
+ HTTP_VERSION_1_1 :
+ `${versionMajor}.${versionMinor}`;
+ incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
+ parser.joinDuplicateHeaders;
+ incoming.url = url;
+ incoming.upgrade = upgrade;
+
+ let n = headers.length;
+
+ // If parser.maxHeaderPairs <= 0 assume that there's no limit.
+ if (parser.maxHeaderPairs > 0)
+ n = MathMin(n, parser.maxHeaderPairs);
+
+ incoming._addHeaderLines(headers, n);
+
+ if (typeof method === 'number') {
+ // server only
+ incoming.method = allMethods[method];
+ } else {
+ // client only
+ incoming.statusCode = statusCode;
+ incoming.statusMessage = statusMessage;
+ }
+
+ return parser.onIncoming(incoming, shouldKeepAlive);
+}
+
+function parserOnBody(b) {
+ const stream = this.incoming;
+
+ // If the stream has already been removed, then drop it.
+ if (stream === null || stream[kSkipPendingData])
+ return;
+
+ // Pretend this was the result of a stream._read call.
+ if (!stream._dumped) {
+ const ret = stream.push(b);
+ if (!ret)
+ readStop(this.socket);
+ }
+}
+
+function parserOnMessageComplete() {
+ const parser = this;
+ const stream = parser.incoming;
+
+ if (stream !== null && !stream[kSkipPendingData]) {
+ stream.complete = true;
+ // Emit any trailing headers.
+ const headers = parser._headers;
+ if (headers.length) {
+ stream._addHeaderLines(headers, headers.length);
+ parser._headers = [];
+ parser._url = '';
+ }
+
+ // For emit end event
+ stream.push(null);
+ }
+
+ // Force to read the next incoming message
+ readStart(parser.socket);
+}
+
+
+const parsers = new FreeList('parsers', 1000, function parsersCb() {
+ const parser = new HTTPParser();
+
+ cleanParser(parser);
+
+ parser[kOnHeaders] = parserOnHeaders;
+ parser[kOnHeadersComplete] = parserOnHeadersComplete;
+ parser[kOnBody] = parserOnBody;
+ parser[kOnMessageComplete] = parserOnMessageComplete;
+
+ return parser;
+});
+
+function closeParserInstance(parser) { parser.close(); }
+
+// Free the parser and also break any links that it
+// might have to any other things.
+// TODO: All parser data should be attached to a
+// single object, so that it can be easily cleaned
+// up by doing `parser.data = {}`, which should
+// be done in FreeList.free. `parsers.free(parser)`
+// should be all that is needed.
+function freeParser(parser, req, socket) {
+ if (parser) {
+ if (parser._consumed)
+ parser.unconsume();
+ parser.remove();
+ cleanParser(parser);
+ if (parsers.free(parser) === false) {
+ // Make sure the parser's stack has unwound before deleting the
+ // corresponding C++ object through .close().
+ setImmediate(closeParserInstance, parser);
+ } else {
+ // Since the Parser destructor isn't going to run the destroy() callbacks
+ // it needs to be triggered manually.
+ parser.free();
+ }
+ }
+ if (req) {
+ req.parser = null;
+ }
+ if (socket) {
+ socket.parser = null;
+ }
+}
+
+// Character code ranges for valid HTTP tokens
+// Valid chars: ^_`a-zA-Z-0-9!#$%&'*+.|~
+// Based on RFC 7230 Section 3.2.6 token definition
+// See https://tools.ietf.org/html/rfc7230#section-3.2.6
+const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
+const validTokenChars = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
+ 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?)
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~)
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 240-255
+]);
+
+/**
+ * Verifies that the given val is a valid HTTP token
+ * per the rules defined in RFC 7230
+ * See https://tools.ietf.org/html/rfc7230#section-3.2.6
+ * @param {string} val
+ * @returns {boolean}
+ */
+function checkIsHttpToken(val) {
+ if (val.length >= 10) {
+ return tokenRegExp.test(val);
+ }
+
+ if (val.length === 0) return false;
+
+ // Use lookup table for short strings, regex for longer ones
+ for (let i = 0; i < val.length; i++) {
+ if (!validTokenChars[val.charCodeAt(i)]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Strict header value regex per RFC 7230 (original/default behavior):
+// field-value = *( field-content / obs-fold )
+// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+// field-vchar = VCHAR / obs-text
+// This rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f).
+const strictHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+
+// Lenient header value regex per Fetch spec (https://fetch.spec.whatwg.org/#header-value):
+// - Must contain no 0x00 (NUL) or HTTP newline bytes (0x0a LF, 0x0d CR)
+// - Must be byte sequences (0x00-0xff), not arbitrary unicode
+// This allows most control characters except NUL, CR, and LF.
+// eslint-disable-next-line no-control-regex
+const lenientHeaderCharRegex = /[\x00\x0a\x0d]|[^\x00-\xff]/;
+
+/**
+ * True if val contains an invalid header value character.
+ * By default uses strict validation per RFC 7230.
+ * When lenient=true, uses relaxed validation per Fetch spec.
+ * @param {string} val
+ * @param {boolean} [lenient] - Use lenient validation (Fetch spec rules)
+ * @returns {boolean}
+ */
+function checkInvalidHeaderChar(val, lenient = false) {
+ const regex = lenient ? lenientHeaderCharRegex : strictHeaderCharRegex;
+ return regex.test(val);
+}
+
+function cleanParser(parser) {
+ parser._headers = [];
+ parser._url = '';
+ parser.socket = null;
+ parser.incoming = null;
+ parser.outgoing = null;
+ parser.maxHeaderPairs = MAX_HEADER_PAIRS;
+ parser[kOnMessageBegin] = null;
+ parser[kOnExecute] = null;
+ parser[kOnTimeout] = null;
+ parser._consumed = false;
+ parser.onIncoming = null;
+ parser.joinDuplicateHeaders = null;
+}
+
+function prepareError(err, parser, rawPacket) {
+ err.rawPacket = rawPacket || parser.getCurrentBuffer();
+ if (typeof err.reason === 'string')
+ err.message = `Parse Error: ${err.reason}`;
+}
+
+let warnedLenient = false;
+
+function isLenient() {
+ if (insecureHTTPParser && !warnedLenient) {
+ warnedLenient = true;
+ process.emitWarning('Using insecure HTTP parsing');
+ }
+ return insecureHTTPParser;
+}
+
+function calculateLenientFlags(httpValidation, insecureHTTPParserOption) {
+ if (httpValidation === 'strict') {
+ return HTTPParser.kLenientNone | 0;
+ } else if (httpValidation === 'relaxed') {
+ return HTTPParser.kLenientHeaderValueRelaxed | 0;
+ } else if (httpValidation === 'insecure') {
+ return HTTPParser.kLenientAll | 0;
+ }
+ const lenient = insecureHTTPParserOption === undefined ?
+ isLenient() : insecureHTTPParserOption;
+ return lenient ? HTTPParser.kLenientAll | 0 : HTTPParser.kLenientNone | 0;
+}
+
+module.exports = {
+ _checkInvalidHeaderChar: checkInvalidHeaderChar,
+ _checkIsHttpToken: checkIsHttpToken,
+ chunkExpression: /(?:^|\W)chunked(?:$|\W)/i,
+ continueExpression: /(?:^|\W)100-continue(?:$|\W)/i,
+ CRLF: '\r\n', // TODO: Deprecate this.
+ freeParser,
+ methods,
+ parsers,
+ kIncomingMessage,
+ HTTPParser,
+ isLenient,
+ calculateLenientFlags,
+ prepareError,
+ kSkipPendingData,
+};
diff --git a/lib/internal/http/incoming.js b/lib/internal/http/incoming.js
new file mode 100644
index 00000000000..0aa7d30580e
--- /dev/null
+++ b/lib/internal/http/incoming.js
@@ -0,0 +1,643 @@
+// 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 {
+ ObjectDefineProperty,
+ ObjectSetPrototypeOf,
+ Symbol,
+} = primordials;
+
+const { Readable, finished } = require('stream');
+const {
+ deprecateInstantiation,
+} = require('internal/util');
+
+const { AbortController } = require('internal/abort_controller');
+
+const kHeaders = Symbol('kHeaders');
+const kHeadersDistinct = Symbol('kHeadersDistinct');
+const kHeadersCount = Symbol('kHeadersCount');
+const kTrailers = Symbol('kTrailers');
+const kTrailersDistinct = Symbol('kTrailersDistinct');
+const kTrailersCount = Symbol('kTrailersCount');
+const kAbortController = Symbol('kAbortController');
+const kAbortSignalSocket = Symbol('kAbortSignalSocket');
+const kAbortSignalListener = Symbol('kAbortSignalListener');
+const kAbortSignalDetached = Symbol('kAbortSignalDetached');
+const kAttachAbortSignal = Symbol('kAttachAbortSignal');
+const kDetachAbortSignal = Symbol('kDetachAbortSignal');
+
+function readStart(socket) {
+ if (socket && !socket._paused && socket.readable)
+ socket.resume();
+}
+
+function readStop(socket) {
+ if (socket)
+ socket.pause();
+}
+
+/* Abstract base class for ServerRequest and ClientResponse. */
+function IncomingMessage(socket) {
+ if (!(this instanceof IncomingMessage)) {
+ return deprecateInstantiation(IncomingMessage, 'DEP0195', socket);
+ }
+
+ Readable.call(this);
+
+ if (socket) {
+ const hwm = socket.readableHighWaterMark;
+ if (this._readableState.highWaterMark !== hwm) {
+ this._readableState.highWaterMark = hwm;
+ }
+ }
+
+ this._readableState.readingMore = true;
+
+ this.socket = socket;
+
+ this.httpVersionMajor = null;
+ this.httpVersionMinor = null;
+ this.httpVersion = null;
+ this.complete = false;
+ this[kHeaders] = null;
+ this[kHeadersCount] = 0;
+ this.rawHeaders = [];
+ this[kTrailers] = null;
+ this[kTrailersCount] = 0;
+ this.rawTrailers = [];
+ this.joinDuplicateHeaders = false;
+ this.aborted = false;
+
+ this.upgrade = null;
+
+ // request (server) only
+ this.url = '';
+ this.method = null;
+
+ // response (client) only
+ this.statusCode = null;
+ this.statusMessage = null;
+ this.client = socket;
+
+ this._consuming = false;
+ // Flag for when we decide that this message cannot possibly be
+ // read by the user, so there's no point continuing to handle it.
+ this._dumped = false;
+ this[kAbortController] = null;
+ this[kAbortSignalSocket] = null;
+ this[kAbortSignalListener] = null;
+ this[kAbortSignalDetached] = false;
+}
+ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype);
+ObjectSetPrototypeOf(IncomingMessage, Readable);
+
+ObjectDefineProperty(IncomingMessage.prototype, 'connection', {
+ __proto__: null,
+ get: function() {
+ return this.socket;
+ },
+ set: function(val) {
+ this.socket = val;
+ },
+});
+
+ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
+ __proto__: null,
+ get: function() {
+ if (!this[kHeaders]) {
+ this[kHeaders] = { __proto__: null };
+
+ const src = this.rawHeaders;
+ const dst = this[kHeaders];
+
+ for (let n = 0; n < this[kHeadersCount]; n += 2) {
+ this._addHeaderLine(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kHeaders];
+ },
+ set: function(val) {
+ this[kHeaders] = val;
+ },
+});
+
+ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
+ __proto__: null,
+ get: function() {
+ if (!this[kHeadersDistinct]) {
+ this[kHeadersDistinct] = { __proto__: null };
+
+ const src = this.rawHeaders;
+ const dst = this[kHeadersDistinct];
+
+ for (let n = 0; n < this[kHeadersCount]; n += 2) {
+ this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kHeadersDistinct];
+ },
+ set: function(val) {
+ this[kHeadersDistinct] = val;
+ },
+});
+
+ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
+ __proto__: null,
+ get: function() {
+ if (!this[kTrailers]) {
+ this[kTrailers] = { __proto__: null };
+
+ const src = this.rawTrailers;
+ const dst = this[kTrailers];
+
+ for (let n = 0; n < this[kTrailersCount]; n += 2) {
+ this._addHeaderLine(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kTrailers];
+ },
+ set: function(val) {
+ this[kTrailers] = val;
+ },
+});
+
+ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
+ __proto__: null,
+ get: function() {
+ if (!this[kTrailersDistinct]) {
+ this[kTrailersDistinct] = { __proto__: null };
+
+ const src = this.rawTrailers;
+ const dst = this[kTrailersDistinct];
+
+ for (let n = 0; n < this[kTrailersCount]; n += 2) {
+ this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kTrailersDistinct];
+ },
+ set: function(val) {
+ this[kTrailersDistinct] = val;
+ },
+});
+
+ObjectDefineProperty(IncomingMessage.prototype, 'signal', {
+ __proto__: null,
+ configurable: true,
+ get: function() {
+ if (this[kAbortController] === null) {
+ const ac = new AbortController();
+ this[kAbortController] = ac;
+ if (this.destroyed && (!this.readableEnded || !this.complete)) {
+ ac.abort();
+ } else {
+ this[kAttachAbortSignal]();
+ }
+ }
+ return this[kAbortController].signal;
+ },
+});
+
+IncomingMessage.prototype[kAttachAbortSignal] = function() {
+ if (this[kAbortController].signal.aborted ||
+ this[kAbortSignalDetached] ||
+ this[kAbortSignalListener] !== null) {
+ return;
+ }
+
+ const socket = this.socket;
+ if (!socket) {
+ return;
+ }
+
+ if (socket.destroyed) {
+ abortSignal(this);
+ return;
+ }
+
+ this[kAbortSignalSocket] = socket;
+ this[kAbortSignalListener] = () => {
+ abortSignal(this);
+ };
+ socket.once('close', this[kAbortSignalListener]);
+};
+
+IncomingMessage.prototype[kDetachAbortSignal] = function() {
+ const socket = this[kAbortSignalSocket];
+ const listener = this[kAbortSignalListener];
+ this[kAbortSignalDetached] = true;
+ this[kAbortSignalSocket] = null;
+ this[kAbortSignalListener] = null;
+ if (socket !== null && listener !== null) {
+ socket.removeListener('close', listener);
+ }
+};
+
+IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
+ if (callback)
+ this.on('timeout', callback);
+ this.socket.setTimeout(msecs);
+ return this;
+};
+
+IncomingMessage.prototype._read = function _read() {
+ if (!this._consuming) {
+ this._readableState.readingMore = false;
+ this._consuming = true;
+ }
+
+ // We actually do almost nothing here, because the parserOnBody
+ // function fills up our internal buffer directly. However, we
+ // do need to unpause the underlying socket so that it flows.
+ if (this.socket.readable)
+ readStart(this.socket);
+};
+
+// 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.
+IncomingMessage.prototype._destroy = function _destroy(err, cb) {
+ if (!this.readableEnded || !this.complete) {
+ this.aborted = true;
+ this.emit('aborted');
+ abortSignal(this);
+ }
+
+ // If aborted and the underlying socket is not already destroyed,
+ // destroy it.
+ // We have to check if the socket is already destroyed because finished
+ // does not call the callback when this method is invoked from `_http_client`
+ // in `test/parallel/test-http-client-spurious-aborted.js`
+ if (this.socket && !this.socket.destroyed && this.aborted) {
+ this.socket.destroy(err);
+ const cleanup = finished(this.socket, (e) => {
+ if (e?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
+ e = null;
+ }
+ cleanup();
+ process.nextTick(onError, this, e || err, cb);
+ });
+ } else if (err == null && !this.aborted) {
+ // The message was received completely and is being destroyed cleanly:
+ // complete the destroy synchronously. 'close' is still emitted on a
+ // later tick by the stream machinery. The deferral below only exists
+ // so that 'error' listeners attached right after destroy(err) still
+ // receive the error, which cannot matter when there is no error.
+ cb();
+ } else {
+ process.nextTick(onError, this, err, cb);
+ }
+};
+
+function abortSignal(self) {
+ self[kDetachAbortSignal]();
+ if (self[kAbortController] !== null) {
+ self[kAbortController].abort();
+ }
+}
+
+IncomingMessage.prototype._addHeaderLines = _addHeaderLines;
+function _addHeaderLines(headers, n) {
+ if (headers?.length) {
+ let dest;
+ if (this.complete) {
+ this.rawTrailers = headers;
+ this[kTrailersCount] = n;
+ dest = this[kTrailers];
+ } else {
+ this.rawHeaders = headers;
+ this[kHeadersCount] = n;
+ dest = this[kHeaders];
+ }
+
+ if (dest) {
+ for (let i = 0; i < n; i += 2) {
+ this._addHeaderLine(headers[i], headers[i + 1], dest);
+ }
+ }
+ }
+}
+
+
+// How repeated instances of a header field are combined in the headers
+// object. Fields that are not known are joined with ', '.
+const kFirstWins = 0;
+const kJoinComma = 1;
+const kJoinSemicolon = 2;
+const kArray = 3;
+
+function knownField(name, merge) {
+ return { name, merge };
+}
+
+// Later values are dropped, unless joinDuplicateHeaders is set.
+const kAge = knownField('age', kFirstWins);
+const kHost = knownField('host', kFirstWins);
+const kFrom = knownField('from', kFirstWins);
+const kETag = knownField('etag', kFirstWins);
+const kServer = knownField('server', kFirstWins);
+const kReferer = knownField('referer', kFirstWins);
+const kExpires = knownField('expires', kFirstWins);
+const kLocation = knownField('location', kFirstWins);
+const kUserAgent = knownField('user-agent', kFirstWins);
+const kRetryAfter = knownField('retry-after', kFirstWins);
+const kContentType = knownField('content-type', kFirstWins);
+const kMaxForwards = knownField('max-forwards', kFirstWins);
+const kAuthorization = knownField('authorization', kFirstWins);
+const kLastModified = knownField('last-modified', kFirstWins);
+const kContentLength = knownField('content-length', kFirstWins);
+const kIfModifiedSince = knownField('if-modified-since', kFirstWins);
+const kProxyAuthorization = knownField('proxy-authorization', kFirstWins);
+const kIfUnmodifiedSince = knownField('if-unmodified-since', kFirstWins);
+
+// Values are joined with ', '.
+const kDate = knownField('date', kJoinComma);
+const kVary = knownField('vary', kJoinComma);
+const kOrigin = knownField('origin', kJoinComma);
+const kExpect = knownField('expect', kJoinComma);
+const kAccept = knownField('accept', kJoinComma);
+const kUpgrade = knownField('upgrade', kJoinComma);
+const kIfMatch = knownField('if-match', kJoinComma);
+const kConnection = knownField('connection', kJoinComma);
+const kCacheControl = knownField('cache-control', kJoinComma);
+const kIfNoneMatch = knownField('if-none-match', kJoinComma);
+const kAcceptEncoding = knownField('accept-encoding', kJoinComma);
+const kAcceptLanguage = knownField('accept-language', kJoinComma);
+const kXForwardedFor = knownField('x-forwarded-for', kJoinComma);
+const kContentEncoding = knownField('content-encoding', kJoinComma);
+const kXForwardedHost = knownField('x-forwarded-host', kJoinComma);
+const kTransferEncoding = knownField('transfer-encoding', kJoinComma);
+const kXForwardedProto = knownField('x-forwarded-proto', kJoinComma);
+
+// Values are joined with '; '.
+const kCookie = knownField('cookie', kJoinSemicolon);
+
+// Values are collected into an array.
+const kSetCookie = knownField('set-cookie', kArray);
+
+// Returns the descriptor of a known field, or the lowercased name of any other
+// field. The 'traditional cased' and lowercase spellings of known fields are
+// matched first to avoid calling toLowerCase() for them.
+// TODO: perhaps http_parser could be returning both raw and lowercased versions
+// of known header names to avoid us having to call toLowerCase() for those
+// headers.
+function matchKnownFields(field, lowercased) {
+ switch (field.length) {
+ case 3:
+ if (field === 'Age' || field === 'age') return kAge;
+ break;
+ case 4:
+ if (field === 'Host' || field === 'host') return kHost;
+ if (field === 'From' || field === 'from') return kFrom;
+ if (field === 'ETag' || field === 'etag') return kETag;
+ if (field === 'Date' || field === 'date') return kDate;
+ if (field === 'Vary' || field === 'vary') return kVary;
+ break;
+ case 6:
+ if (field === 'Server' || field === 'server') return kServer;
+ if (field === 'Cookie' || field === 'cookie') return kCookie;
+ if (field === 'Origin' || field === 'origin') return kOrigin;
+ if (field === 'Expect' || field === 'expect') return kExpect;
+ if (field === 'Accept' || field === 'accept') return kAccept;
+ break;
+ case 7:
+ if (field === 'Referer' || field === 'referer') return kReferer;
+ if (field === 'Expires' || field === 'expires') return kExpires;
+ if (field === 'Upgrade' || field === 'upgrade') return kUpgrade;
+ break;
+ case 8:
+ if (field === 'Location' || field === 'location')
+ return kLocation;
+ if (field === 'If-Match' || field === 'if-match')
+ return kIfMatch;
+ break;
+ case 10:
+ if (field === 'User-Agent' || field === 'user-agent')
+ return kUserAgent;
+ if (field === 'Set-Cookie' || field === 'set-cookie')
+ return kSetCookie;
+ if (field === 'Connection' || field === 'connection')
+ return kConnection;
+ break;
+ case 11:
+ if (field === 'Retry-After' || field === 'retry-after')
+ return kRetryAfter;
+ break;
+ case 12:
+ if (field === 'Content-Type' || field === 'content-type')
+ return kContentType;
+ if (field === 'Max-Forwards' || field === 'max-forwards')
+ return kMaxForwards;
+ break;
+ case 13:
+ if (field === 'Authorization' || field === 'authorization')
+ return kAuthorization;
+ if (field === 'Last-Modified' || field === 'last-modified')
+ return kLastModified;
+ if (field === 'Cache-Control' || field === 'cache-control')
+ return kCacheControl;
+ if (field === 'If-None-Match' || field === 'if-none-match')
+ return kIfNoneMatch;
+ break;
+ case 14:
+ if (field === 'Content-Length' || field === 'content-length')
+ return kContentLength;
+ break;
+ case 15:
+ if (field === 'Accept-Encoding' || field === 'accept-encoding')
+ return kAcceptEncoding;
+ if (field === 'Accept-Language' || field === 'accept-language')
+ return kAcceptLanguage;
+ if (field === 'X-Forwarded-For' || field === 'x-forwarded-for')
+ return kXForwardedFor;
+ break;
+ case 16:
+ if (field === 'Content-Encoding' || field === 'content-encoding')
+ return kContentEncoding;
+ if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host')
+ return kXForwardedHost;
+ break;
+ case 17:
+ if (field === 'If-Modified-Since' || field === 'if-modified-since')
+ return kIfModifiedSince;
+ if (field === 'Transfer-Encoding' || field === 'transfer-encoding')
+ return kTransferEncoding;
+ if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto')
+ return kXForwardedProto;
+ break;
+ case 19:
+ if (field === 'Proxy-Authorization' || field === 'proxy-authorization')
+ return kProxyAuthorization;
+ if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since')
+ return kIfUnmodifiedSince;
+ break;
+ }
+ if (lowercased) {
+ return field;
+ }
+ return matchKnownFields(field.toLowerCase(), true);
+}
+// Add the given (field, value) pair to the message
+//
+// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
+// same header with a ', ' if the header in question supports specification of
+// multiple values this way. The one exception to this is the Cookie header,
+// which has multiple values joined with a '; ' instead. If a header's values
+// cannot be joined in either of these ways, we declare the first instance the
+// winner and drop the second. Fields that are not known are always joined.
+IncomingMessage.prototype._addHeaderLine = _addHeaderLine;
+function _addHeaderLine(field, value, dest) {
+ const known = matchKnownFields(field);
+ let merge = kJoinComma;
+ if (typeof known === 'string') {
+ field = known;
+ } else {
+ field = known.name;
+ merge = known.merge;
+ }
+ if (merge === kJoinComma || merge === kJoinSemicolon) {
+ // Make a delimited list
+ if (typeof dest[field] === 'string') {
+ dest[field] += (merge === kJoinComma ? ', ' : '; ') + value;
+ } else {
+ dest[field] = value;
+ }
+ } else if (merge === kArray) {
+ // Array header -- only Set-Cookie at the moment
+ if (dest['set-cookie'] !== undefined) {
+ dest['set-cookie'].push(value);
+ } else {
+ dest['set-cookie'] = [value];
+ }
+ } else if (this.joinDuplicateHeaders) {
+ // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
+ // https://github.com/nodejs/node/issues/45699
+ // allow authorization multiple fields
+ // Make a delimited list
+ if (dest[field] === undefined) {
+ dest[field] = value;
+ } else {
+ dest[field] += ', ' + value;
+ }
+ } else if (dest[field] === undefined) {
+ // Drop duplicates
+ dest[field] = value;
+ }
+}
+
+IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct;
+function _addHeaderLineDistinct(field, value, dest) {
+ field = field.toLowerCase();
+ if (!dest[field]) {
+ dest[field] = [value];
+ } else {
+ dest[field].push(value);
+ }
+}
+
+IncomingMessage.prototype._dumpAndCloseReadable = function _dumpAndCloseReadable() {
+ this._dumped = true;
+ this._readableState.ended = true;
+ this._readableState.endEmitted = true;
+ this._readableState.destroyed = true;
+ this._readableState.closed = true;
+ this._readableState.closeEmitted = true;
+};
+
+
+// Call this instead of resume() if we want to just
+// dump all the data to /dev/null
+IncomingMessage.prototype._dump = function _dump() {
+ if (!this._dumped) {
+ this._dumped = true;
+ // If there is buffered data, it may trigger 'data' events.
+ // Remove 'data' event listeners explicitly.
+ this.removeAllListeners('data');
+ if (this.complete && !this.destroyed && this.readableLength === 0) {
+ // The message was fully received and never read: there is nothing to
+ // pull off the wire. Go straight to the 'end' emission instead of
+ // paying for the resume() and flow() scheduling machinery.
+ this.read(0);
+ } else {
+ this.resume();
+ }
+ }
+};
+
+// Case-insensitive ASCII compare against an already-lowercased name.
+// Avoids allocating a lowercased copy of every header name.
+function asciiEqualIgnoreCase(a, lower) {
+ const len = lower.length;
+ if (a.length !== len)
+ return false;
+ if (a === lower)
+ return true;
+ for (let i = 0; i < len; i++) {
+ let c = a.charCodeAt(i);
+ if (c >= 65 && c <= 90)
+ c += 32;
+ if (c !== lower.charCodeAt(i))
+ return false;
+ }
+ return true;
+}
+
+function getRawHeader(msg, lowerName, joinDuplicates) {
+ const rawHeaders = msg.rawHeaders;
+ const count = msg[kHeadersCount];
+ let result;
+ for (let i = 0; i < count; i += 2) {
+ if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
+ continue;
+ const value = rawHeaders[i + 1];
+ if (result === undefined) {
+ result = value;
+ if (!joinDuplicates)
+ return result;
+ } else {
+ result += ', ' + value;
+ }
+ }
+ return result;
+}
+
+function onError(self, error, cb) {
+ // This is to keep backward compatible behavior.
+ // An error is emitted only if there are listeners attached to the event.
+ if (self.listenerCount('error') === 0) {
+ cb();
+ } else {
+ cb(error);
+ }
+}
+
+module.exports = {
+ IncomingMessage,
+ kDetachAbortSignal,
+ kHeadersCount,
+ asciiEqualIgnoreCase,
+ getRawHeader,
+ readStart,
+ readStop,
+};
diff --git a/lib/internal/http/outgoing.js b/lib/internal/http/outgoing.js
new file mode 100644
index 00000000000..ad0ad6dc372
--- /dev/null
+++ b/lib/internal/http/outgoing.js
@@ -0,0 +1,1489 @@
+// 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 {
+ 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, utcDateHeader, kNeedDrain } = require('internal/http');
+const { Buffer } = require('buffer');
+const {
+ _checkIsHttpToken: checkIsHttpToken,
+ _checkInvalidHeaderChar: checkInvalidHeaderChar,
+ chunkExpression: RE_TE_CHUNKED,
+ isLenient,
+} = require('internal/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,
+ ERR_UNKNOWN_ENCODING,
+ },
+ hideStackFrames,
+} = require('internal/errors');
+const {
+ validateObject,
+ validateOneOf,
+ 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');
+
+// Container for the headers set through `setHeader()`, keyed by the
+// lower-cased header name. It has no enumerable or inherited properties, like
+// a `{ __proto__: null }` literal, but instances are fast-mode objects: V8
+// creates null-prototype literals in dictionary mode, which costs a hash table
+// allocation per response and slower property access for every header.
+class OutgoingHeaders {}
+ObjectSetPrototypeOf(OutgoingHeaders.prototype, null);
+delete OutgoingHeaders.prototype.constructor;
+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 kLenientCache = Symbol('kLenientCache');
+
+let keepAliveTimeoutCache = -1;
+let keepAliveMaxCache = -1;
+let keepAliveHeaderCache = '';
+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);
+ }
+
+ // Preset the listener table with the events that are commonly listened
+ // for, mirroring streams. The presence of the properties keeps `_events`
+ // in fast mode: the first `on()` call would otherwise have to allocate a
+ // dictionary for the `{ __proto__: null }` object created by EventEmitter.
+ this._events ??= {
+ close: undefined,
+ error: undefined,
+ finish: undefined,
+ prefinish: undefined,
+ drain: undefined,
+ };
+
+ 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;
+ this[kLenientCache] = null;
+}
+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() {
+ // 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 (msg.httpValidation !== undefined) {
+ return msg.httpValidation !== 'strict';
+ }
+ // ServerResponse: check server's httpValidation option
+ const serverHttpValidation = msg.req?.socket?.server?.httpValidation;
+ if (serverHttpValidation !== undefined) {
+ return serverHttpValidation !== 'strict';
+ }
+ // Legacy insecureHTTPParser - ClientRequest has it directly
+ if (typeof msg.insecureHTTPParser === 'boolean') {
+ return msg.insecureHTTPParser;
+ }
+ // ServerResponse can access via req.socket.server
+ 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,
+ 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() {
+ let bufferedLength = this[kChunkedLength];
+ if (bufferedLength !== 0) {
+ bufferedLength += bufferedLength.toString(16).length + 4 +
+ (!this._headerSent && this._header !== null ? this._header.length : 0);
+ }
+ return this.outputSize + bufferedLength + (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]--;
+ const socket = this[kSocket];
+ try {
+ if (this[kCorked] || this[kChunkedBuffer].length === 0) {
+ return;
+ }
+
+ const len = this[kChunkedLength];
+ const buf = this[kChunkedBuffer];
+ if (this.destroyed || socket?.destroyed) {
+ const error = this[kErrored] || socket?._writableState?.errored || new ERR_STREAM_DESTROYED('write');
+ for (let n = 2; n < buf.length; n += 3) {
+ if (buf[n] !== nop) {
+ process.nextTick(buf[n], error);
+ }
+ }
+ buf.length = 0;
+ this[kChunkedLength] = 0;
+ return;
+ }
+ assert(this.chunkedEncoding);
+
+ const 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);
+ callbacks.push(buf[n + 2]);
+ }
+ this._send(crlf_buf, null, (err) => {
+ for (const callback of callbacks) {
+ callback(err);
+ }
+ });
+
+ this[kChunkedBuffer].length = 0;
+ this[kChunkedLength] = 0;
+ } finally {
+ socket?.uncork();
+ }
+
+ // 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[kChunkedBuffer].length !== 0) {
+ this[kCorked] = 1;
+ this.uncork();
+ }
+
+ 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 += utcDateHeader();
+ }
+
+ // 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) {
+ if (this._keepAliveTimeout && this._defaultKeepAlive) {
+ // 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 += keepAliveHeaderCache;
+ } else {
+ header += 'Connection: keep-alive\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 lowerOutgoingHeaderName(field) {
+ switch (field) {
+ case 'Connection':
+ case 'connection':
+ return 'connection';
+ case 'Content-Length':
+ case 'content-length':
+ return 'content-length';
+ case 'Transfer-Encoding':
+ case 'transfer-encoding':
+ return 'transfer-encoding';
+ case 'Date':
+ case 'date':
+ return 'date';
+ case 'Expect':
+ case 'expect':
+ return 'expect';
+ case 'Trailer':
+ case 'trailer':
+ return 'trailer';
+ case 'Keep-Alive':
+ case 'keep-alive':
+ return 'keep-alive';
+ default:
+ return field.toLowerCase();
+ }
+}
+
+function matchHeader(self, state, field, value) {
+ if (field.length < 4 || field.length > 17)
+ return;
+ field = lowerOutgoingHeaderName(field);
+ 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 (!isValidHeaderName(name)) {
+ throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name);
+ }
+}));
+
+/**
+ * Non-throwing counterpart of `validateHeaderName()`.
+ * @param {any} name
+ * @returns {boolean}
+ */
+function isValidHeaderName(name) {
+ return typeof name === 'string' && checkIsHttpToken(name);
+}
+
+const kHttpValidationValues = ['strict', 'relaxed'];
+
+/**
+ * Non-throwing counterpart of `validateHeaderValue()`.
+ * @param {any} value
+ * @param {{ httpValidation?: 'strict' | 'relaxed' }} [options]
+ * @returns {boolean}
+ */
+function isValidHeaderValue(value, options) {
+ let lenient = false;
+ if (options !== undefined) {
+ validateObject(options, 'options');
+ const { httpValidation } = options;
+ if (httpValidation === 'relaxed') {
+ lenient = true;
+ } else if (httpValidation !== undefined && httpValidation !== 'strict') {
+ validateOneOf(httpValidation, 'options.httpValidation', kHttpValidationValues);
+ }
+ }
+ if (value === undefined || typeof value === 'symbol') {
+ return false;
+ }
+ return !checkInvalidHeaderChar(value, lenient);
+}
+
+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 = new OutgoingHeaders();
+
+ 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 headers = ObjectValues(headersMap);
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ for (let i = 0; i < headers.length; i++) {
+ headers[i] = headers[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 (msg.chunkedEncoding && typeof chunk === 'string' && encoding && !Buffer.isEncoding(encoding)) {
+ throw new ERR_UNKNOWN_ENCODING(encoding);
+ }
+
+ if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
+ msg.cork();
+ process.nextTick(connectionCorkNT, msg);
+ }
+
+ let ret;
+ if (msg.chunkedEncoding && chunk.length !== 0) {
+ len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
+ if (msg[kCorked]) {
+ if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) {
+ chunk = Stream._uint8ArrayToBuffer(chunk);
+ }
+ msg[kChunkedBuffer].push(chunk, encoding, callback);
+ msg[kChunkedLength] += len;
+ ret = msg.writableLength < msg.writableHighWaterMark;
+ } 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 if (msg.chunkedEncoding && msg[kCorked] && msg[kChunkedBuffer].length !== 0) {
+ msg[kChunkedBuffer].push(chunk, encoding, callback);
+ ret = msg.writableLength < msg.writableHighWaterMark;
+ } else {
+ ret = msg._send(chunk, encoding, callback, len);
+ }
+
+ debug('write ret = ' + ret);
+ return ret;
+}
+
+
+// If this last write can be delivered immediately as the final chunk, this
+// prepares to do so, and then returns true. If not, it returns false and
+// a separate _send call and tick will be required to finish up.
+function maybePrepareFinalChunk(msg, chunk, encoding) {
+ if (typeof chunk !== 'string' && !isUint8Array(chunk))
+ return false;
+
+ if (msg.destroyed || msg.strictContentLength)
+ return false;
+
+ if (!msg._header) {
+ msg._contentLength = typeof chunk === 'string' ?
+ Buffer.byteLength(chunk, encoding) : chunk.byteLength;
+ msg._implicitHeader();
+ }
+
+ return !!msg._header && msg._hasBody && !msg.chunkedEncoding;
+}
+
+function connectionCorkNT(msg) {
+ if (msg[kCorked]) {
+ msg.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;
+ }
+
+ let finishCallback = 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();
+ }
+
+ if (maybePrepareFinalChunk(this, chunk, encoding)) {
+ // If just one final write is required, with nothing to follow, we
+ // attach finish to the write to avoid a separate send() & tick step
+ // later on - this is purely a performance optimization.
+ if (typeof callback === 'function') {
+ queueEndCallback(this, callback);
+ callback = undefined;
+ }
+ finishCallback = onFinish.bind(undefined, this);
+ }
+
+ write_(this, chunk, encoding, finishCallback, 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);
+ }
+
+ // Flush buffered chunks before terminating the response.
+ if (this[kChunkedBuffer].length !== 0) {
+ this[kSocket]?.cork();
+ this[kCorked] = 1;
+ this.uncork();
+ }
+
+ if (finishCallback === null) {
+ // If we didn't early finish, send the last data and schedule 'finish' now:
+ finishCallback = onFinish.bind(undefined, this);
+
+ if (this._hasBody && this.chunkedEncoding) {
+ this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finishCallback);
+ } else if (!this._headerSent || this.writableLength || chunk) {
+ this._send('', 'latin1', finishCallback);
+ } else {
+ process.nextTick(finishCallback);
+ }
+ }
+
+ 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,
+ isValidHeaderName,
+ isValidHeaderValue,
+ validateHeaderName,
+ validateHeaderValue,
+ OutgoingMessage,
+};
diff --git a/lib/internal/http/server.js b/lib/internal/http/server.js
new file mode 100644
index 00000000000..406eede9300
--- /dev/null
+++ b/lib/internal/http/server.js
@@ -0,0 +1,1549 @@
+// 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 {
+ ArrayIsArray,
+ Error,
+ MathMin,
+ NumberIsFinite,
+ ObjectKeys,
+ ObjectSetPrototypeOf,
+ ReflectApply,
+ Symbol,
+ SymbolAsyncDispose,
+ SymbolFor,
+} = primordials;
+
+const { Duplex } = require('stream');
+const net = require('net');
+const EE = require('events');
+const assert = require('internal/assert');
+const {
+ parsers,
+ freeParser,
+ continueExpression,
+ chunkExpression,
+ kIncomingMessage,
+ kSocket,
+ HTTPParser,
+ calculateLenientFlags,
+ _checkInvalidHeaderChar: checkInvalidHeaderChar,
+ prepareError,
+} = require('internal/http/common');
+const { ConnectionsList } = internalBinding('http_parser');
+const {
+ kUniqueHeaders,
+ parseUniqueHeadersOption,
+ OutgoingMessage,
+ validateHeaderName,
+ validateHeaderValue,
+} = require('internal/http/outgoing');
+const {
+ kOutHeaders,
+ kNeedDrain,
+ isTraceHTTPEnabled,
+ traceBegin,
+ traceEnd,
+ getNextTraceEventId,
+} = require('internal/http');
+const {
+ defaultTriggerAsyncIdScope,
+ getOrSetAsyncId,
+} = require('internal/async_hooks');
+const {
+ IncomingMessage,
+ kDetachAbortSignal,
+ kHeadersCount,
+ asciiEqualIgnoreCase,
+ getRawHeader,
+} = require('internal/http/incoming');
+const {
+ ConnResetException,
+ codes: {
+ ERR_HTTP_HEADERS_SENT,
+ ERR_HTTP_INVALID_STATUS_CODE,
+ ERR_HTTP_REQUEST_TIMEOUT,
+ ERR_HTTP_SOCKET_ASSIGNED,
+ ERR_HTTP_SOCKET_ENCODING,
+ ERR_INVALID_ARG_VALUE,
+ ERR_INVALID_CHAR,
+ ERR_OUT_OF_RANGE,
+ },
+} = require('internal/errors');
+const {
+ assignFunctionName,
+ deprecateInstantiation,
+ kEmptyObject,
+ promisify,
+} = require('internal/util');
+const {
+ validateInteger,
+ validateBoolean,
+ validateOneOf,
+ validateLinkHeaderValue,
+ validateObject,
+ validateFunction,
+} = require('internal/validators');
+const Buffer = require('buffer').Buffer;
+const { setInterval, clearInterval } = require('timers');
+let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
+ debug = fn;
+});
+
+const dc = require('diagnostics_channel');
+const onRequestStartChannel = dc.channel('http.server.request.start');
+const onResponseCreatedChannel = dc.channel('http.server.response.created');
+const onResponseFinishChannel = dc.channel('http.server.response.finish');
+
+const kServerResponse = Symbol('ServerResponse');
+const kServerResponseStatistics = Symbol('ServerResponseStatistics');
+const kUpgradeStream = Symbol('UpgradeStream');
+
+const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
+const kConnectionState = Symbol('ConnectionState');
+const kResponseOptions = Symbol('ResponseOptions');
+
+const statusLineCache = [];
+
+const {
+ hasObserver,
+ startPerf,
+ stopPerf,
+} = require('internal/perf/observe');
+
+const STATUS_CODES = {
+ 100: 'Continue', // RFC 7231 6.2.1
+ 101: 'Switching Protocols', // RFC 7231 6.2.2
+ 102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918)
+ 103: 'Early Hints', // RFC 8297 2
+ 200: 'OK', // RFC 7231 6.3.1
+ 201: 'Created', // RFC 7231 6.3.2
+ 202: 'Accepted', // RFC 7231 6.3.3
+ 203: 'Non-Authoritative Information', // RFC 7231 6.3.4
+ 204: 'No Content', // RFC 7231 6.3.5
+ 205: 'Reset Content', // RFC 7231 6.3.6
+ 206: 'Partial Content', // RFC 7233 4.1
+ 207: 'Multi-Status', // RFC 4918 11.1
+ 208: 'Already Reported', // RFC 5842 7.1
+ 226: 'IM Used', // RFC 3229 10.4.1
+ 300: 'Multiple Choices', // RFC 7231 6.4.1
+ 301: 'Moved Permanently', // RFC 7231 6.4.2
+ 302: 'Found', // RFC 7231 6.4.3
+ 303: 'See Other', // RFC 7231 6.4.4
+ 304: 'Not Modified', // RFC 7232 4.1
+ 305: 'Use Proxy', // RFC 7231 6.4.5
+ 307: 'Temporary Redirect', // RFC 7231 6.4.7
+ 308: 'Permanent Redirect', // RFC 7238 3
+ 400: 'Bad Request', // RFC 7231 6.5.1
+ 401: 'Unauthorized', // RFC 7235 3.1
+ 402: 'Payment Required', // RFC 7231 6.5.2
+ 403: 'Forbidden', // RFC 7231 6.5.3
+ 404: 'Not Found', // RFC 7231 6.5.4
+ 405: 'Method Not Allowed', // RFC 7231 6.5.5
+ 406: 'Not Acceptable', // RFC 7231 6.5.6
+ 407: 'Proxy Authentication Required', // RFC 7235 3.2
+ 408: 'Request Timeout', // RFC 7231 6.5.7
+ 409: 'Conflict', // RFC 7231 6.5.8
+ 410: 'Gone', // RFC 7231 6.5.9
+ 411: 'Length Required', // RFC 7231 6.5.10
+ 412: 'Precondition Failed', // RFC 7232 4.2
+ 413: 'Payload Too Large', // RFC 7231 6.5.11
+ 414: 'URI Too Long', // RFC 7231 6.5.12
+ 415: 'Unsupported Media Type', // RFC 7231 6.5.13
+ 416: 'Range Not Satisfiable', // RFC 7233 4.4
+ 417: 'Expectation Failed', // RFC 7231 6.5.14
+ 418: 'I\'m a Teapot', // RFC 7168 2.3.3
+ 421: 'Misdirected Request', // RFC 7540 9.1.2
+ 422: 'Unprocessable Entity', // RFC 4918 11.2
+ 423: 'Locked', // RFC 4918 11.3
+ 424: 'Failed Dependency', // RFC 4918 11.4
+ 425: 'Too Early', // RFC 8470 5.2
+ 426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15
+ 428: 'Precondition Required', // RFC 6585 3
+ 429: 'Too Many Requests', // RFC 6585 4
+ 431: 'Request Header Fields Too Large', // RFC 6585 5
+ 451: 'Unavailable For Legal Reasons', // RFC 7725 3
+ 500: 'Internal Server Error', // RFC 7231 6.6.1
+ 501: 'Not Implemented', // RFC 7231 6.6.2
+ 502: 'Bad Gateway', // RFC 7231 6.6.3
+ 503: 'Service Unavailable', // RFC 7231 6.6.4
+ 504: 'Gateway Timeout', // RFC 7231 6.6.5
+ 505: 'HTTP Version Not Supported', // RFC 7231 6.6.6
+ 506: 'Variant Also Negotiates', // RFC 2295 8.1
+ 507: 'Insufficient Storage', // RFC 4918 11.5
+ 508: 'Loop Detected', // RFC 5842 7.2
+ 509: 'Bandwidth Limit Exceeded',
+ 510: 'Not Extended', // RFC 2774 7
+ 511: 'Network Authentication Required', // RFC 6585 6
+};
+
+const kOnExecute = HTTPParser.kOnExecute | 0;
+const kOnTimeout = HTTPParser.kOnTimeout | 0;
+
+const kConnections = Symbol('http.server.connections');
+const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval');
+
+const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request';
+
+class HTTPServerAsyncResource {
+ constructor(type, socket) {
+ this.type = type;
+ this.socket = socket;
+ }
+}
+
+function ServerResponse(req, options) {
+ if (!(this instanceof ServerResponse)) {
+ return deprecateInstantiation(ServerResponse, 'DEP0195', req, options);
+ }
+
+ OutgoingMessage.call(this, options);
+
+ if (req.method === 'HEAD') this._hasBody = false;
+
+ this.req = req;
+ this.sendDate = true;
+ this._sent100 = false;
+ this._expect_continue = false;
+
+ if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
+ this.useChunkedEncodingByDefault =
+ chunkExpression.test(getRawHeader(req, 'te', true));
+ this.shouldKeepAlive = false;
+ }
+
+ if (hasObserver('http')) {
+ startPerf(this, kServerResponseStatistics, {
+ type: 'http',
+ name: 'HttpRequest',
+ detail: {
+ req: {
+ method: req.method,
+ url: req.url,
+ headers: req.headers,
+ },
+ },
+ });
+ }
+ if (isTraceHTTPEnabled()) {
+ this._traceEventId = getNextTraceEventId();
+ traceBegin(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId);
+ }
+ if (onResponseCreatedChannel.hasSubscribers) {
+ onResponseCreatedChannel.publish({
+ request: req,
+ response: this,
+ });
+ }
+}
+ObjectSetPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype);
+ObjectSetPrototypeOf(ServerResponse, OutgoingMessage);
+
+ServerResponse.prototype._finish = function _finish() {
+ if (this[kServerResponseStatistics] && hasObserver('http')) {
+ stopPerf(this, kServerResponseStatistics, {
+ detail: {
+ res: {
+ statusCode: this.statusCode,
+ statusMessage: this.statusMessage,
+ headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {},
+ },
+ },
+ });
+ }
+ OutgoingMessage.prototype._finish.call(this);
+ if (isTraceHTTPEnabled() && typeof this._traceEventId === 'number') {
+ const data = {
+ url: this.req?.url,
+ statusCode: this.statusCode,
+ };
+ traceEnd(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId, data);
+ }
+};
+
+
+ServerResponse.prototype.statusCode = 200;
+ServerResponse.prototype.statusMessage = undefined;
+
+function onServerResponseClose() {
+ // EventEmitter.emit makes a copy of the 'close' listeners array before
+ // calling the listeners. detachSocket() unregisters onServerResponseClose
+ // but if detachSocket() is called, directly or indirectly, by a 'close'
+ // listener, onServerResponseClose is still in that copy of the listeners
+ // array. That is, in the example below, b still gets called even though
+ // it's been removed by a:
+ //
+ // const EventEmitter = require('events');
+ // const obj = new EventEmitter();
+ // obj.on('event', a);
+ // obj.on('event', b);
+ // function a() { obj.removeListener('event', b) }
+ // function b() { throw "BAM!" }
+ // obj.emit('event'); // throws
+ //
+ // Ergo, we need to deal with stale 'close' events and handle the case
+ // where the ServerResponse object has already been deconstructed.
+ // Fortunately, that requires only a single if check. :-)
+ if (this._httpMessage) {
+ emitCloseNT(this._httpMessage);
+ }
+}
+
+ServerResponse.prototype.assignSocket = function assignSocket(socket) {
+ if (socket._httpMessage) {
+ throw new ERR_HTTP_SOCKET_ASSIGNED();
+ }
+ socket._httpMessage = this;
+ socket.on('close', onServerResponseClose);
+ this.socket = socket;
+ this.emit('socket', socket);
+ this._flush();
+};
+
+ServerResponse.prototype.detachSocket = function detachSocket(socket) {
+ assert(socket._httpMessage === this);
+ socket.removeListener('close', onServerResponseClose);
+ socket._httpMessage = null;
+ this.socket = null;
+};
+
+ServerResponse.prototype.writeInformation = function writeInformation(
+ statusCode, headers, cb) {
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT('write');
+ }
+
+ validateInteger(statusCode, 'statusCode', 100, 199);
+ if (statusCode === 101) {
+ throw new ERR_HTTP_INVALID_STATUS_CODE(statusCode);
+ }
+
+ const statusMessage = STATUS_CODES[statusCode] || 'unknown';
+ let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`;
+
+ const lenient = this._isLenientHeaderValidation();
+ if (headers !== undefined && headers !== null) {
+ if (ArrayIsArray(headers)) {
+ if (headers.length && ArrayIsArray(headers[0])) {
+ for (let i = 0; i < headers.length; i++) {
+ const entry = headers[i];
+ head += processInformationHeader(entry[0], entry[1], lenient);
+ }
+ } else {
+ if (headers.length % 2 !== 0) {
+ throw new ERR_INVALID_ARG_VALUE('headers', headers);
+ }
+ for (let i = 0; i < headers.length; i += 2) {
+ head += processInformationHeader(headers[i], headers[i + 1], lenient);
+ }
+ }
+ } else {
+ validateObject(headers, 'headers');
+ const keys = ObjectKeys(headers);
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ head += processInformationHeader(key, headers[key], lenient);
+ }
+ }
+ }
+
+ head += '\r\n';
+
+ return this._writeRaw(head, 'ascii', cb);
+};
+
+function processInformationHeader(name, value, lenient) {
+ validateHeaderName(name);
+ validateHeaderValue(name, value, lenient);
+ return `${name}: ${value}\r\n`;
+}
+
+ServerResponse.prototype.writeContinue = function writeContinue(cb) {
+ this.writeInformation(100, null, cb);
+ this._sent100 = true;
+};
+
+ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
+ this.writeInformation(102, null, cb);
+};
+
+ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
+ validateObject(hints, 'hints');
+
+ if (hints.link === null || hints.link === undefined) {
+ return;
+ }
+
+ const link = validateLinkHeaderValue(hints.link);
+
+ if (link.length === 0) {
+ return;
+ }
+
+ if (checkInvalidHeaderChar(link)) {
+ throw new ERR_INVALID_CHAR('header content', 'Link');
+ }
+
+ const headers = { __proto__: null, Link: link };
+ const keys = ObjectKeys(hints);
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ if (key !== 'link') {
+ headers[key] = hints[key];
+ }
+ }
+
+ this.writeInformation(103, headers, cb);
+};
+
+ServerResponse.prototype._implicitHeader = function _implicitHeader() {
+ this.writeHead(this.statusCode);
+};
+
+ServerResponse.prototype.writeHead = writeHead;
+function writeHead(statusCode, reason, obj) {
+
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT('write');
+ }
+
+ const originalStatusCode = statusCode;
+
+ statusCode |= 0;
+ if (statusCode < 100 || statusCode > 999) {
+ throw new ERR_HTTP_INVALID_STATUS_CODE(originalStatusCode);
+ }
+
+
+ if (typeof reason === 'string') {
+ // writeHead(statusCode, reasonPhrase[, headers])
+ this.statusMessage = reason;
+ } else {
+ // writeHead(statusCode[, headers])
+ this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown';
+ obj ??= reason;
+ }
+ this.statusCode = statusCode;
+
+ let headers;
+ if (this[kOutHeaders]) {
+ // Slow-case: when progressive API and header fields are passed.
+ let k;
+ if (ArrayIsArray(obj)) {
+ if (obj.length % 2 !== 0) {
+ throw new ERR_INVALID_ARG_VALUE('headers', obj);
+ }
+
+ // Headers in obj should override previous headers but still
+ // allow explicit duplicates. To do so, we first remove any
+ // existing conflicts, then use appendHeader.
+
+ for (let n = 0; n < obj.length; n += 2) {
+ k = obj[n + 0];
+ this.removeHeader(k);
+ }
+
+ for (let n = 0; n < obj.length; n += 2) {
+ k = obj[n + 0];
+ if (k) this.appendHeader(k, obj[n + 1]);
+ }
+ } else if (obj) {
+ const keys = ObjectKeys(obj);
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ for (let i = 0; i < keys.length; i++) {
+ k = keys[i];
+ if (k) this.setHeader(k, obj[k]);
+ }
+ }
+ // Only progressive api is used
+ headers = this[kOutHeaders];
+ } else {
+ // Only writeHead() called
+ headers = obj;
+ }
+
+ 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');
+
+ statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
+ }
+
+ if (statusCode === 204 || statusCode === 304 ||
+ (statusCode >= 100 && statusCode <= 199)) {
+ // RFC 2616, 10.2.5:
+ // The 204 response MUST NOT include a message-body, and thus is always
+ // terminated by the first empty line after the header fields.
+ // RFC 2616, 10.3.5:
+ // The 304 response MUST NOT contain a message-body, and thus is always
+ // terminated by the first empty line after the header fields.
+ // RFC 2616, 10.1 Informational 1xx:
+ // This class of status code indicates a provisional response,
+ // consisting only of the Status-Line and optional headers, and is
+ // terminated by an empty line.
+ this._hasBody = false;
+ }
+
+ // Don't keep alive connections where the client expects 100 Continue
+ // but we sent a final status; they may put extra bytes on the wire.
+ if (this._expect_continue && !this._sent100) {
+ this.shouldKeepAlive = false;
+ }
+
+ this._storeHeader(statusLine, headers);
+
+ return this;
+}
+
+function storeHTTPOptions(options) {
+ this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
+ this[kServerResponse] = options.ServerResponse || ServerResponse;
+
+ const maxHeaderSize = options.maxHeaderSize;
+ if (maxHeaderSize !== undefined)
+ validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
+ this.maxHeaderSize = maxHeaderSize;
+
+ const optimizeEmptyRequests = options.optimizeEmptyRequests;
+ if (optimizeEmptyRequests !== undefined)
+ validateBoolean(optimizeEmptyRequests, 'options.optimizeEmptyRequests');
+ this[kOptimizeEmptyRequests] = optimizeEmptyRequests || false;
+
+ const insecureHTTPParser = options.insecureHTTPParser;
+ if (insecureHTTPParser !== undefined)
+ validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
+ this.insecureHTTPParser = insecureHTTPParser;
+
+ const httpValidation = options.httpValidation;
+ if (httpValidation !== undefined) {
+ validateOneOf(httpValidation, 'options.httpValidation',
+ ['strict', 'relaxed', 'insecure']);
+ if (insecureHTTPParser !== undefined) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.httpValidation',
+ httpValidation,
+ 'cannot be used together with options.insecureHTTPParser',
+ );
+ }
+ }
+ this.httpValidation = httpValidation;
+
+ const requestTimeout = options.requestTimeout;
+ if (requestTimeout !== undefined) {
+ validateInteger(requestTimeout, 'requestTimeout', 0);
+ this.requestTimeout = requestTimeout;
+ } else {
+ this.requestTimeout = 300_000; // 5 minutes
+ }
+
+ const headersTimeout = options.headersTimeout;
+ if (headersTimeout !== undefined) {
+ validateInteger(headersTimeout, 'headersTimeout', 0);
+ this.headersTimeout = headersTimeout;
+ } else {
+ this.headersTimeout = MathMin(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout
+ }
+
+ if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) {
+ throw new ERR_OUT_OF_RANGE('headersTimeout', '<= requestTimeout', headersTimeout);
+ }
+
+ const keepAliveTimeout = options.keepAliveTimeout;
+ if (keepAliveTimeout !== undefined) {
+ validateInteger(keepAliveTimeout, 'keepAliveTimeout', 0);
+ this.keepAliveTimeout = keepAliveTimeout;
+ } else {
+ this.keepAliveTimeout = 65_000; // 65 seconds;
+ }
+
+ const keepAliveTimeoutBuffer = options.keepAliveTimeoutBuffer;
+ if (keepAliveTimeoutBuffer !== undefined) {
+ validateInteger(keepAliveTimeoutBuffer, 'keepAliveTimeoutBuffer', 0);
+ this.keepAliveTimeoutBuffer = keepAliveTimeoutBuffer;
+ } else {
+ this.keepAliveTimeoutBuffer = 1000;
+ }
+
+ const connectionsCheckingInterval = options.connectionsCheckingInterval;
+ if (connectionsCheckingInterval !== undefined) {
+ validateInteger(connectionsCheckingInterval, 'connectionsCheckingInterval', 0);
+ this.connectionsCheckingInterval = connectionsCheckingInterval;
+ } else {
+ this.connectionsCheckingInterval = 30_000; // 30 seconds
+ }
+
+ const requireHostHeader = options.requireHostHeader;
+ if (requireHostHeader !== undefined) {
+ validateBoolean(requireHostHeader, 'options.requireHostHeader');
+ this.requireHostHeader = requireHostHeader;
+ } else {
+ this.requireHostHeader = true;
+ }
+
+ const joinDuplicateHeaders = options.joinDuplicateHeaders;
+ if (joinDuplicateHeaders !== undefined) {
+ validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
+ }
+ this.joinDuplicateHeaders = joinDuplicateHeaders;
+
+ const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites;
+ if (rejectNonStandardBodyWrites !== undefined) {
+ validateBoolean(rejectNonStandardBodyWrites, 'options.rejectNonStandardBodyWrites');
+ this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites;
+ } else {
+ this.rejectNonStandardBodyWrites = false;
+ }
+
+ const shouldUpgradeCallback = options.shouldUpgradeCallback;
+ if (shouldUpgradeCallback !== undefined) {
+ validateFunction(shouldUpgradeCallback, 'options.shouldUpgradeCallback');
+ this.shouldUpgradeCallback = shouldUpgradeCallback;
+ } else {
+ this.shouldUpgradeCallback = function() {
+ return this.listenerCount('upgrade') > 0;
+ };
+ }
+}
+
+function setupConnectionsTracking() {
+ // Start connection handling
+ this[kConnections] ||= new ConnectionsList();
+
+ if (this[kConnectionsCheckingInterval]) {
+ clearInterval(this[kConnectionsCheckingInterval]);
+ }
+ // This checker is started without checking whether any headersTimeout or requestTimeout is non zero
+ // otherwise it would not be started if such timeouts are modified after createServer.
+ this[kConnectionsCheckingInterval] =
+ setInterval(checkConnections.bind(this), this.connectionsCheckingInterval).unref();
+}
+
+function httpServerPreClose(server) {
+ server.closeIdleConnections();
+ clearInterval(server[kConnectionsCheckingInterval]);
+}
+
+function Server(options, requestListener) {
+ if (!(this instanceof Server)) {
+ return deprecateInstantiation(Server, 'DEP0195', options, requestListener);
+ }
+
+ if (typeof options === 'function') {
+ requestListener = options;
+ options = kEmptyObject;
+ } else if (options == null) {
+ options = kEmptyObject;
+ } else {
+ validateObject(options, 'options');
+ }
+
+ storeHTTPOptions.call(this, options);
+
+ net.Server.call(
+ this,
+ { allowHalfOpen: true, noDelay: options.noDelay ?? true,
+ keepAlive: options.keepAlive,
+ keepAliveInitialDelay: options.keepAliveInitialDelay,
+ highWaterMark: options.highWaterMark });
+
+ if (requestListener) {
+ this.on('request', requestListener);
+ }
+
+ // Similar option to this. Too lazy to write my own docs.
+ // http://www.squid-cache.org/Doc/config/half_closed_clients/
+ // https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
+ this.httpAllowHalfOpen = false;
+
+ this.on('connection', connectionListener);
+ this.on('listening', setupConnectionsTracking);
+
+ this.timeout = 0;
+ this.maxHeadersCount = null;
+ this.maxRequestsPerSocket = 0;
+
+ this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
+}
+ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
+ObjectSetPrototypeOf(Server, net.Server);
+
+Server.prototype.close = function close() {
+ httpServerPreClose(this);
+ ReflectApply(net.Server.prototype.close, this, arguments);
+ return this;
+};
+
+Server.prototype[SymbolAsyncDispose] = assignFunctionName(SymbolAsyncDispose, async function() {
+ await promisify(this.close).call(this);
+});
+
+Server.prototype.closeAllConnections = function closeAllConnections() {
+ if (!this[kConnections]) {
+ return;
+ }
+
+ const connections = this[kConnections].all();
+
+ for (let i = 0, l = connections.length; i < l; i++) {
+ connections[i].socket.destroy();
+ }
+};
+
+Server.prototype.closeIdleConnections = function closeIdleConnections() {
+ if (!this[kConnections]) {
+ return;
+ }
+
+ const connections = this[kConnections].idle();
+
+ for (let i = 0, l = connections.length; i < l; i++) {
+ if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) {
+ continue;
+ }
+
+ connections[i].socket.destroy();
+ }
+};
+
+Server.prototype.setTimeout = function setTimeout(msecs, callback) {
+ this.timeout = msecs;
+ if (callback)
+ this.on('timeout', callback);
+ return this;
+};
+
+Server.prototype[EE.captureRejectionSymbol] =
+assignFunctionName(EE.captureRejectionSymbol, function(err, event, ...args) {
+ switch (event) {
+ case 'request': {
+ const { 1: res } = args;
+ if (!res.headersSent && !res.writableEnded) {
+ // Don't leak headers.
+ const names = res.getHeaderNames();
+ for (let i = 0; i < names.length; i++) {
+ res.removeHeader(names[i]);
+ }
+ res.statusCode = 500;
+ res.end(STATUS_CODES[500]);
+ } else {
+ res.destroy();
+ }
+ break;
+ }
+ default:
+ ReflectApply(
+ net.Server.prototype[SymbolFor('nodejs.rejection')],
+ this, arguments);
+ }
+});
+
+function checkConnections() {
+ const headersTimeout =
+ NumberIsFinite(this.headersTimeout) && this.headersTimeout >= 0 ?
+ this.headersTimeout : 0;
+ const requestTimeout =
+ NumberIsFinite(this.requestTimeout) && this.requestTimeout >= 0 ?
+ this.requestTimeout : 0;
+
+ if (headersTimeout === 0 && requestTimeout === 0) {
+ return;
+ }
+
+ const expired = this[kConnections].expired(headersTimeout, requestTimeout);
+
+ for (let i = 0; i < expired.length; i++) {
+ const socket = expired[i].socket;
+
+ if (socket) {
+ onRequestTimeout(socket);
+ }
+ }
+}
+
+function connectionListener(socket) {
+ defaultTriggerAsyncIdScope(
+ getOrSetAsyncId(socket), connectionListenerInternal, this, socket,
+ );
+}
+
+function connectionListenerInternal(server, socket) {
+ debug('SERVER new http connection');
+
+ // Ensure that the server property of the socket is correctly set.
+ // See https://github.com/nodejs/node/issues/13435
+ socket.server = server;
+
+ // If the user has added a listener to the server,
+ // request, or response, then it's their responsibility.
+ // otherwise, destroy on timeout by default
+ if (server.timeout && typeof socket.setTimeout === 'function')
+ socket.setTimeout(server.timeout);
+ socket.on('timeout', socketOnTimeout);
+
+ const parser = parsers.alloc();
+
+ const lenientFlags = calculateLenientFlags(server.httpValidation, server.insecureHTTPParser);
+
+ // Propagate headers limit from server instance to parser
+ if (typeof server.maxHeadersCount === 'number') {
+ parser.maxHeaderPairs = server.maxHeadersCount << 1;
+ }
+
+ // TODO(addaleax): This doesn't play well with the
+ // `async_hooks.currentResource()` proposal, see
+ // https://github.com/nodejs/node/pull/21313
+ parser.initialize(
+ HTTPParser.REQUEST,
+ new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket),
+ server.maxHeaderSize || 0,
+ lenientFlags,
+ server[kConnections],
+ parser.maxHeaderPairs,
+ );
+ parser.socket = socket;
+ socket.parser = parser;
+
+ const state = {
+ onData: null,
+ onEnd: null,
+ onClose: null,
+ onDrain: null,
+ outgoing: [],
+ incoming: [],
+ // `outgoingData` is an approximate amount of bytes queued through all
+ // inactive responses. If more data than the high watermark is queued - we
+ // need to pause TCP socket/HTTP parser, and wait until the data will be
+ // sent to the client.
+ 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,
+ server, socket, parser, state);
+ state.onClose = socketOnClose.bind(undefined,
+ socket, state);
+ state.onDrain = socketOnDrain.bind(undefined,
+ socket, state);
+ socket.on('data', state.onData);
+ socket.on('error', socketOnError);
+ socket.on('end', state.onEnd);
+ socket.on('close', state.onClose);
+ socket.on('drain', state.onDrain);
+ parser.onIncoming = parserOnIncoming.bind(undefined,
+ server, socket, state);
+
+ // We are consuming socket, so it won't get any actual data
+ socket.on('resume', onSocketResume);
+ socket.on('pause', onSocketPause);
+
+ // Overrides to unconsume on `data`, `readable` listeners
+ socket.on = generateSocketListenerWrapper('on');
+ socket.addListener = generateSocketListenerWrapper('addListener');
+ socket.prependListener = generateSocketListenerWrapper('prependListener');
+ socket.setEncoding = socketSetEncoding;
+
+ // We only consume the socket if it has never been consumed before.
+ if (socket._handle?.isStreamBase &&
+ !socket._handle._consumed) {
+ parser._consumed = true;
+ socket._handle._consumed = true;
+ parser.consume(socket._handle);
+ }
+ parser[kOnExecute] =
+ onParserExecute.bind(undefined,
+ server, socket, parser, state);
+
+ parser[kOnTimeout] =
+ onParserTimeout.bind(undefined,
+ server, socket);
+
+ socket._paused = false;
+}
+
+function socketSetEncoding() {
+ throw new ERR_HTTP_SOCKET_ENCODING();
+}
+
+function updateOutgoingData(socket, state, delta) {
+ state.outgoingData += delta;
+ socketOnDrain(socket, state);
+}
+
+function socketOnDrain(socket, state) {
+ const needPause = state.outgoingData > socket.writableHighWaterMark;
+
+ // If we previously paused, then start reading again.
+ if (socket._paused && !needPause) {
+ socket._paused = false;
+ if (socket.parser)
+ socket.parser.resume();
+ socket.resume();
+ }
+
+ const msg = socket._httpMessage;
+ // Only emit 'drain' once the message has no data pending anywhere, so that
+ // msg.writableLength === 0 when the event fires. socketOnDrain is called
+ // synchronously from updateOutgoingData during _flushOutput, at which point
+ // the bytes we just handed to the socket (or the stale outputSize) mean
+ // the message is not actually drained yet - we wait for the socket's
+ // own 'drain' event instead.
+ if (msg && !msg.finished && msg[kNeedDrain] && msg.writableLength === 0) {
+ msg[kNeedDrain] = false;
+ msg.emit('drain');
+ }
+}
+
+function socketOnTimeout() {
+ const req = this.parser?.incoming;
+ const reqTimeout = req && !req.complete && req.emit('timeout', this);
+ const res = this._httpMessage;
+ const resTimeout = res && res.emit('timeout', this);
+ const serverTimeout = this.server.emit('timeout', this);
+
+ if (!reqTimeout && !resTimeout && !serverTimeout)
+ this.destroy();
+}
+
+function socketOnClose(socket, state) {
+ debug('server socket close');
+ freeParser(socket.parser, null, socket);
+ abortIncoming(state.incoming);
+ abortOutgoing(state.outgoing);
+}
+
+function abortIncoming(incoming) {
+ while (incoming.length) {
+ const req = incoming.shift();
+ req.destroy(new ConnResetException('aborted'));
+ }
+}
+
+function abortOutgoing(outgoing) {
+ while (outgoing.length) {
+ const req = outgoing.shift();
+ req.destroy(new ConnResetException('aborted'));
+ }
+}
+
+function socketOnEnd(server, socket, parser, state) {
+ const ret = parser.finish();
+
+ if (ret instanceof Error) {
+ debug('parse error');
+ // socketOnError has additional logic and will call socket.destroy(err).
+ socketOnError.call(socket, ret);
+ } else if (!server.httpAllowHalfOpen) {
+ socket.end();
+ } else if (state.outgoing.length) {
+ state.outgoing[state.outgoing.length - 1]._last = true;
+ } else if (socket._httpMessage) {
+ socket._httpMessage._last = true;
+ } else {
+ socket.end();
+ }
+}
+
+function socketOnData(server, socket, parser, state, d) {
+ assert(!socket._paused);
+ debug('SERVER socketOnData %d', d.length);
+
+ const ret = parser.execute(d);
+ onParserExecuteCommon(server, socket, parser, state, ret, d);
+}
+
+function onRequestTimeout(socket) {
+ // socketOnError has additional logic and will call socket.destroy(err).
+ socketOnError.call(socket, new ERR_HTTP_REQUEST_TIMEOUT());
+}
+
+function onParserExecute(server, socket, parser, state, ret) {
+ // When underlying `net.Socket` instance is consumed - no
+ // `data` events are emitted, and thus `socket.setTimeout` fires the
+ // callback even if the data is constantly flowing into the socket.
+ // See, https://github.com/nodejs/node/commit/ec2822adaad76b126b5cccdeaa1addf2376c9aa6
+ socket._unrefTimer();
+ debug('SERVER socketOnParserExecute %d', ret);
+ onParserExecuteCommon(server, socket, parser, state, ret, undefined);
+}
+
+function onParserTimeout(server, socket) {
+ const serverTimeout = server.emit('timeout', socket);
+
+ if (!serverTimeout)
+ socket.destroy();
+}
+
+const noop = () => {};
+const badRequestResponse = Buffer.from(
+ `HTTP/1.1 400 ${STATUS_CODES[400]}\r\n` +
+ 'Connection: close\r\n\r\n', 'ascii',
+);
+const requestTimeoutResponse = Buffer.from(
+ `HTTP/1.1 408 ${STATUS_CODES[408]}\r\n` +
+ 'Connection: close\r\n\r\n', 'ascii',
+);
+const requestHeaderFieldsTooLargeResponse = Buffer.from(
+ `HTTP/1.1 431 ${STATUS_CODES[431]}\r\n` +
+ 'Connection: close\r\n\r\n', 'ascii',
+);
+
+const requestChunkExtensionsTooLargeResponse = Buffer.from(
+ `HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` +
+ 'Connection: close\r\n\r\n', 'ascii',
+);
+
+function socketOnError(e) {
+ // Ignore further errors
+ this.removeListener('error', socketOnError);
+
+ if (this.listenerCount('error', noop) === 0) {
+ this.on('error', noop);
+ }
+
+ if (!this.server.emit('clientError', e, this)) {
+ // Caution must be taken to avoid corrupting the remote peer.
+ // Reply an error segment if there is no in-flight `ServerResponse`,
+ // or no data of the in-flight one has been written yet to this socket.
+ if (this.writable &&
+ (!this._httpMessage || !this._httpMessage._headerSent)) {
+ let response;
+
+ switch (e.code) {
+ case 'HPE_HEADER_OVERFLOW':
+ response = requestHeaderFieldsTooLargeResponse;
+ break;
+ case 'HPE_CHUNK_EXTENSIONS_OVERFLOW':
+ response = requestChunkExtensionsTooLargeResponse;
+ break;
+ case 'ERR_HTTP_REQUEST_TIMEOUT':
+ response = requestTimeoutResponse;
+ break;
+ default:
+ response = badRequestResponse;
+ break;
+ }
+
+ this.write(response);
+ }
+ this.destroy(e);
+ }
+}
+
+class UpgradeStream extends Duplex {
+ constructor(socket, req) {
+ super({
+ allowHalfOpen: socket.allowHalfOpen,
+ });
+
+ this[kSocket] = socket;
+ this[kIncomingMessage] = req;
+
+ // Proxy error, end & closure events immediately.
+ socket.on('error', (err) => this.destroy(err));
+
+ socket.on('close', () => this.destroy());
+ this.on('close', () => socket.destroy());
+
+ socket.on('end', () => {
+ this.push(null);
+
+ // Match the socket behaviour, where 'end' will fire despite no 'data'
+ // listeners if a socket with no pending data ends:
+ if (this.readableLength === 0) {
+ this.resume();
+ }
+ });
+
+ // Other events (most notably, reading) all only
+ // activate after requestBodyCompleted is called.
+ }
+
+ requestBodyCompleted(upgradeHead) {
+ this[kIncomingMessage] = null;
+
+ // When the request body is completed, we begin streaming all the
+ // post-body data for the upgraded protocol:
+ if (upgradeHead?.length > 0) {
+ if (!this.push(upgradeHead)) {
+ this[kSocket].pause();
+ }
+ }
+
+ this[kSocket].on('data', (data) => {
+ if (!this.push(data)) {
+ this[kSocket].pause();
+ }
+ });
+ }
+
+ _read(size) {
+ // Reading the upgrade stream starts the request stream flowing. It's
+ // important that this happens, even if there are no listeners, or it
+ // would be impossible to read this without explicitly reading all the
+ // request body first, which is backward incompatible & awkward.
+ this[kIncomingMessage]?.resume();
+
+ this[kSocket].resume();
+ }
+
+ _final(callback) {
+ this[kSocket].end(callback);
+ }
+
+ _write(chunk, encoding, callback) {
+ this[kSocket].write(chunk, encoding, callback);
+ }
+
+ _destroy(err, callback) {
+ this[kSocket].destroy(err);
+ callback(err);
+ }
+}
+
+function onParserExecuteCommon(server, socket, parser, state, ret, d) {
+ if (ret instanceof Error) {
+ prepareError(ret, parser, d);
+ debug('parse error', ret);
+ socketOnError.call(socket, ret);
+ } else if (parser.incoming?.upgrade) {
+ // Upgrade or CONNECT
+ const req = parser.incoming;
+ debug('SERVER upgrade or connect', req.method);
+ const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
+
+ let upgradeStream;
+ if (req.complete) {
+ d ||= parser.getCurrentBuffer();
+
+ socket.removeListener('data', state.onData);
+ socket.removeListener('end', state.onEnd);
+ socket.removeListener('close', state.onClose);
+ socket.removeListener('drain', state.onDrain);
+ socket.removeListener('error', socketOnError);
+ socket.removeListener('timeout', socketOnTimeout);
+
+ unconsume(parser, socket);
+ parser.finish();
+ freeParser(parser, req, socket);
+ parser = null;
+
+ // If the request is complete (no body, or all body read upfront) then
+ // we just emit the socket directly as the upgrade stream.
+ upgradeStream = socket;
+ } else {
+ // If the body hasn't been fully parsed yet, we emit immediately but
+ // we add a wrapper around the socket to not expose incoming data
+ // until the request body has finished.
+
+ if (socket[kUpgradeStream]) {
+ // We've already emitted the incomplete upgrade - nothing do to
+ // until actual body parsing completion.
+ return;
+ }
+
+ d ||= Buffer.alloc(0);
+
+ upgradeStream = new UpgradeStream(socket, req);
+ socket[kUpgradeStream] = upgradeStream;
+ }
+
+ if (server.listenerCount(eventName) > 0) {
+ debug('SERVER have listener for %s', eventName);
+
+ const bodyHead = d.slice(ret, d.length);
+
+ if (req.complete && socket[kUpgradeStream]) {
+ // Previously emitted, now completed - just activate the stream
+ socket[kUpgradeStream].requestBodyCompleted(bodyHead);
+ } else {
+ socket.readableFlowing = null;
+ server.emit(eventName, req, upgradeStream, bodyHead);
+ }
+ } else {
+ // Got upgrade or CONNECT method, but have no handler.
+ socket.destroy();
+ }
+ } else if (parser.incoming && parser.incoming.method === 'PRI') {
+ debug('SERVER got PRI request');
+ socket.destroy();
+ }
+
+ if (socket._paused && socket.parser) {
+ // onIncoming paused the socket, we should pause the parser as well
+ debug('pause parser');
+ socket.parser.pause();
+ }
+}
+
+function clearIncoming(req) {
+ req ||= this;
+ const parser = req.socket?.parser;
+ // Reset the .incoming property so that the request object can be gc'ed.
+ if (parser && parser.incoming === req) {
+ if (req.readableEnded) {
+ parser.incoming = null;
+ } else {
+ req.on('end', clearIncoming);
+ }
+ }
+}
+
+function resOnFinish(req, res, socket, state, server) {
+ if (onResponseFinishChannel.hasSubscribers) {
+ onResponseFinishChannel.publish({
+ request: req,
+ response: res,
+ socket,
+ server,
+ });
+ }
+
+ // Usually the first incoming element should be our request. it may
+ // be that in the case abortIncoming() was called that the incoming
+ // array will be empty.
+ assert(state.incoming.length === 0 || state.incoming[0] === req);
+
+ req[kDetachAbortSignal]();
+ state.incoming.shift();
+
+ // If the user never called req.read(), and didn't pipe() or
+ // .resume() or .on('data'), then we call req._dump() so that the
+ // bytes will be pulled off the wire.
+ if (!req._consuming && !req._readableState.resumeScheduled)
+ req._dump();
+
+ res.detachSocket(socket);
+ clearIncoming(req);
+ process.nextTick(emitCloseNT, res);
+
+ if (res._last) {
+ if (typeof socket.destroySoon === 'function') {
+ socket.destroySoon();
+ } else {
+ socket.end();
+ }
+ } else if (state.outgoing.length === 0) {
+ const keepAliveTimeout = NumberIsFinite(server.keepAliveTimeout) && server.keepAliveTimeout >= 0 ?
+ server.keepAliveTimeout : 0;
+ const keepAliveTimeoutBuffer = NumberIsFinite(server.keepAliveTimeoutBuffer) && server.keepAliveTimeoutBuffer >= 0 ?
+ server.keepAliveTimeoutBuffer : 1e3;
+
+ if (keepAliveTimeout && typeof socket.setTimeout === 'function') {
+ // Extend the internal timeout by the configured buffer to reduce
+ // the likelihood of ECONNRESET errors.
+ // This allows fine-tuning beyond the advertised keepAliveTimeout.
+ socket.setTimeout(keepAliveTimeout + keepAliveTimeoutBuffer);
+ state.keepAliveTimeoutSet = true;
+ }
+ } else {
+ // Start sending the next message
+ const m = state.outgoing.shift();
+ if (m) {
+ m.assignSocket(socket);
+ }
+ }
+}
+
+function emitCloseNT(self) {
+ if (!self._closed) {
+ self.destroyed = true;
+ self._closed = true;
+ self.emit('close');
+ }
+}
+
+// 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 && asciiEqualIgnoreCase(key, 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 key = rawHeaders[i];
+ const length = key.length;
+ if (length === 14) {
+ if (asciiEqualIgnoreCase(key, 'content-length'))
+ return true;
+ } else if (length === 17) {
+ if (asciiEqualIgnoreCase(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
+// new message. In this callback we setup the response object and pass it
+// to the user.
+function parserOnIncoming(server, socket, state, req, keepAlive) {
+ resetSocketTimeout(server, socket, state);
+
+ if (req.upgrade) {
+ req.upgrade = req.method === 'CONNECT' ||
+ !!server.shouldUpgradeCallback(req);
+ if (req.upgrade) {
+ return 0;
+ }
+ }
+
+ state.incoming.push(req);
+
+ // If the writable end isn't consuming, then stop reading
+ // so that we don't become overwhelmed by a flood of
+ // pipelined requests that may never be resolved.
+ if (!socket._paused) {
+ const ws = socket._writableState;
+ if (ws.needDrain || state.outgoingData >= socket.writableHighWaterMark) {
+ socket._paused = true;
+ // We also need to pause the parser, but don't do that until after
+ // the call to execute, because we may still be processing the last
+ // chunk.
+ socket.pause();
+ }
+ }
+
+ const res = new server[kServerResponse](req, getResponseOptions(server, socket));
+ res._keepAliveTimeout = server.keepAliveTimeout;
+ res._maxRequestsPerSocket = server.maxRequestsPerSocket;
+ res._onPendingData = state.onPendingData;
+ res[kConnectionState] = state;
+
+ res.shouldKeepAlive = keepAlive;
+ res[kUniqueHeaders] = server[kUniqueHeaders];
+
+ if (onRequestStartChannel.hasSubscribers) {
+ onRequestStartChannel.publish({
+ request: req,
+ response: res,
+ socket,
+ server,
+ });
+ }
+
+ // Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
+ const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req);
+
+ if (shouldOptimize) {
+ // Fast processing where emitting 'data', 'end' and 'close' events is
+ // skipped and data is dumped.
+ // This avoids a lot of unnecessary overhead otherwise introduced by
+ // stream.Readable life cycle rules. The downside is that this will
+ // break some servers that read bodies for methods that don't have body headers.
+ req._dumpAndCloseReadable();
+ req._read();
+ }
+
+ if (socket._httpMessage) {
+ // There are already pending outgoing res, append.
+ state.outgoing.push(res);
+ } else {
+ res.assignSocket(socket);
+ }
+
+ // When we're finished writing the response, check if this is the last
+ // response, if so destroy the socket.
+ res.on('finish', onResponseFinish);
+
+ let handled = false;
+
+
+ if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
+
+ // 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 && !hasRequestHeader(req, 4, 'host')) {
+ res.writeHead(400, ['Connection', 'close']);
+ res.end();
+ return 0;
+ }
+
+ const isRequestsLimitSet = (
+ typeof server.maxRequestsPerSocket === 'number' &&
+ server.maxRequestsPerSocket > 0
+ );
+
+ if (isRequestsLimitSet) {
+ state.requestsCount++;
+ res.maxRequestsOnConnectionReached = (
+ server.maxRequestsPerSocket <= state.requestsCount);
+ }
+
+ if (isRequestsLimitSet &&
+ (server.maxRequestsPerSocket < state.requestsCount)) {
+ handled = true;
+ server.emit('dropRequest', req, socket);
+ res.writeHead(503);
+ res.end();
+ } else {
+ const expect = getRawHeader(req, 'expect', true);
+ if (expect !== undefined) {
+ handled = true;
+
+ if (continueExpression.test(expect)) {
+ res._expect_continue = true;
+ if (server.listenerCount('checkContinue') > 0) {
+ server.emit('checkContinue', req, res);
+ } else {
+ res.writeContinue();
+ server.emit('request', req, res);
+ }
+ } else if (server.listenerCount('checkExpectation') > 0) {
+ server.emit('checkExpectation', req, res);
+ } else {
+ res.writeHead(417);
+ res.end();
+ }
+ }
+ }
+ }
+
+ if (!handled) {
+ server.emit('request', req, res);
+ }
+
+ return 0; // No special treatment.
+}
+
+function resetSocketTimeout(server, socket, state) {
+ if (!state.keepAliveTimeoutSet)
+ return;
+
+ socket.setTimeout(server.timeout || 0);
+ state.keepAliveTimeoutSet = false;
+}
+
+function onSocketResume() {
+ // It may seem that the socket is resumed, but this is an enemy's trick to
+ // deceive us! `resume` is emitted asynchronously, and may be called from
+ // `incoming.readStart()`. Stop the socket again here, just to preserve the
+ // state.
+ //
+ // We don't care about stream semantics for the consumed socket anyway.
+ if (this._paused) {
+ this.pause();
+ return;
+ }
+
+ if (this._handle && !this._handle.reading) {
+ this._handle.reading = true;
+ this._handle.readStart();
+ }
+}
+
+function onSocketPause() {
+ if (this._handle?.reading) {
+ this._handle.reading = false;
+ this._handle.readStop();
+ }
+}
+
+function unconsume(parser, socket) {
+ if (socket._handle) {
+ if (parser._consumed)
+ parser.unconsume();
+ parser._consumed = false;
+ socket.removeListener('pause', onSocketPause);
+ socket.removeListener('resume', onSocketResume);
+ }
+}
+
+function generateSocketListenerWrapper(originalFnName) {
+ return function socketListenerWrap(ev, fn) {
+ const res = net.Socket.prototype[originalFnName].call(this,
+ ev, fn);
+ if (!this.parser) {
+ this.on = net.Socket.prototype.on;
+ this.addListener = net.Socket.prototype.addListener;
+ this.prependListener = net.Socket.prototype.prependListener;
+ return res;
+ }
+
+ if (ev === 'data' || ev === 'readable')
+ unconsume(this.parser, this);
+
+ return res;
+ };
+}
+
+module.exports = {
+ STATUS_CODES,
+ Server,
+ ServerResponse,
+ setupConnectionsTracking,
+ storeHTTPOptions,
+ _connectionListener: connectionListener,
+ kServerResponse,
+ httpServerPreClose,
+ kConnectionsCheckingInterval,
+};
diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js
index 85abfa714fa..407d84e7fc9 100644
--- a/lib/internal/http2/compat.js
+++ b/lib/internal/http2/compat.js
@@ -60,7 +60,8 @@ const {
assertValidPseudoHeader,
getAuthority,
} = require('internal/http2/util');
-const { _checkIsHttpToken: checkIsHttpToken } = require('_http_common');
+
+const { _checkIsHttpToken: checkIsHttpToken } = require('internal/http/common');
const kBeginSend = Symbol('begin-send');
const kState = Symbol('state');
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 9f14c5f2aef..011cf22210f 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -53,7 +53,7 @@ const {
httpServerPreClose,
setupConnectionsTracking,
storeHTTPOptions,
-} = require('_http_server');
+} = require('internal/http/server');
const JSStreamSocket = require('internal/js_stream_socket');
const {
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index e2e6288b9ec..8007b300bd1 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -16,7 +16,7 @@ const {
const {
_checkIsHttpToken: checkIsHttpToken,
-} = require('_http_common');
+} = require('internal/http/common');
const binding = internalBinding('http2');
const {
diff --git a/test/async-hooks/test-httpparser.request.js b/test/async-hooks/test-httpparser.request.js
index 40c01bc8177..6379bf330d2 100644
--- a/test/async-hooks/test-httpparser.request.js
+++ b/test/async-hooks/test-httpparser.request.js
@@ -1,3 +1,5 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
const common = require('../common');
@@ -9,7 +11,7 @@ const { checkInvocations } = require('./hook-checks');
const hooks = initHooks();
hooks.enable();
-const { HTTPParser } = require('_http_common');
+const { HTTPParser } = require('internal/http/common');
const REQUEST = HTTPParser.REQUEST;
diff --git a/test/async-hooks/test-httpparser.response.js b/test/async-hooks/test-httpparser.response.js
index 64f0893c5a9..2e600b12747 100644
--- a/test/async-hooks/test-httpparser.response.js
+++ b/test/async-hooks/test-httpparser.response.js
@@ -1,3 +1,5 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
const common = require('../common');
@@ -10,7 +12,7 @@ const hooks = initHooks();
hooks.enable();
-const { HTTPParser } = require('_http_common');
+const { HTTPParser } = require('internal/http/common');
const RESPONSE = HTTPParser.RESPONSE;
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
diff --git a/test/parallel/test-async-local-storage-http-parser-leak.js b/test/parallel/test-async-local-storage-http-parser-leak.js
index 2992db7c73f..b55aed8717a 100644
--- a/test/parallel/test-async-local-storage-http-parser-leak.js
+++ b/test/parallel/test-async-local-storage-http-parser-leak.js
@@ -1,11 +1,12 @@
-// Flags: --expose-gc
+// Flags: --expose-gc --expose-internals
+
'use strict';
const common = require('../common');
const { onGC } = require('../common/gc');
const assert = require('node:assert');
const { AsyncLocalStorage } = require('node:async_hooks');
-const { freeParser, parsers, HTTPParser } = require('_http_common');
+const { freeParser, parsers, HTTPParser } = require('internal/http/common');
let storeGCed = false;
diff --git a/test/parallel/test-http-agent-keepalive-delay.js b/test/parallel/test-http-agent-keepalive-delay.js
index 17a0c00c095..c15cdc6f594 100644
--- a/test/parallel/test-http-agent-keepalive-delay.js
+++ b/test/parallel/test-http-agent-keepalive-delay.js
@@ -3,7 +3,7 @@
const common = require('../common');
const assert = require('assert');
const http = require('http');
-const { Agent } = require('_http_agent');
+const { Agent } = require('http');
const agent = new Agent({
keepAlive: true,
diff --git a/test/parallel/test-http-agent-keepalive.js b/test/parallel/test-http-agent-keepalive.js
index 1cf5a597d0a..ca50fa40bf2 100644
--- a/test/parallel/test-http-agent-keepalive.js
+++ b/test/parallel/test-http-agent-keepalive.js
@@ -23,7 +23,7 @@
const common = require('../common');
const assert = require('assert');
const http = require('http');
-const Agent = require('_http_agent').Agent;
+const Agent = require('http').Agent;
let name;
diff --git a/test/parallel/test-http-common.js b/test/parallel/test-http-common.js
index 1629856ce57..33b7d28ec13 100644
--- a/test/parallel/test-http-common.js
+++ b/test/parallel/test-http-common.js
@@ -1,7 +1,9 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
require('../common');
const assert = require('assert');
-const httpCommon = require('_http_common');
+const httpCommon = require('internal/http/common');
const checkIsHttpToken = httpCommon._checkIsHttpToken;
const checkInvalidHeaderChar = httpCommon._checkInvalidHeaderChar;
diff --git a/test/parallel/test-http-header-value-relaxed.js b/test/parallel/test-http-header-value-relaxed.js
index d002b808963..68311f3ce96 100644
--- a/test/parallel/test-http-header-value-relaxed.js
+++ b/test/parallel/test-http-header-value-relaxed.js
@@ -1,10 +1,12 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const { duplexPair } = require('stream');
-const { HTTPParser } = require('_http_common');
+const { HTTPParser } = require('internal/http/common');
// llhttp_set_lenient_header_value_relaxed() was added in llhttp 9.4.0.
// On shared-library builds using an older system llhttp the constant is
// exported as 0, so inbound-parsing tests must be skipped there.
diff --git a/test/parallel/test-http-invalidheaderfield2.js b/test/parallel/test-http-invalidheaderfield2.js
index c40de68011a..f1f83550a5a 100644
--- a/test/parallel/test-http-invalidheaderfield2.js
+++ b/test/parallel/test-http-invalidheaderfield2.js
@@ -1,8 +1,10 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
require('../common');
const assert = require('assert');
const inspect = require('util').inspect;
-const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('_http_common');
+const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('internal/http/common');
// Good header field names
[
diff --git a/test/parallel/test-http-parser-bad-ref.js b/test/parallel/test-http-parser-bad-ref.js
index e34054eca67..381f2cc2f99 100644
--- a/test/parallel/test-http-parser-bad-ref.js
+++ b/test/parallel/test-http-parser-bad-ref.js
@@ -2,11 +2,11 @@
// Run this program with valgrind or efence with --expose_gc to expose the
// problem.
-// Flags: --expose_gc
+// Flags: --expose_gc --expose-internals --no-warnings
require('../common');
const assert = require('assert');
-const { HTTPParser } = require('_http_common');
+const { HTTPParser } = require('internal/http/common');
const kOnHeaders = HTTPParser.kOnHeaders | 0;
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
diff --git a/test/parallel/test-http-parser-lazy-loaded.js b/test/parallel/test-http-parser-lazy-loaded.js
index 44bb59f052e..4b8d7b696cc 100644
--- a/test/parallel/test-http-parser-lazy-loaded.js
+++ b/test/parallel/test-http-parser-lazy-loaded.js
@@ -20,7 +20,7 @@ binding.HTTPParser = DummyParser;
const assert = require('assert');
const { spawn } = require('child_process');
-const { parsers } = require('_http_common');
+const { parsers } = require('internal/http/common');
// Test _http_common was not loaded before monkey patching
const parser = parsers.alloc();
diff --git a/test/parallel/test-http-parser-memory-retention.js b/test/parallel/test-http-parser-memory-retention.js
index 316797fec3e..29d8dda98dd 100644
--- a/test/parallel/test-http-parser-memory-retention.js
+++ b/test/parallel/test-http-parser-memory-retention.js
@@ -1,9 +1,10 @@
+// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
-const { HTTPParser } = require('_http_common');
+const { HTTPParser } = require('internal/http/common');
// Test that the `HTTPParser` instance is cleaned up before being returned to
// the pool to avoid memory retention issues.
diff --git a/test/parallel/test-http-parser.js b/test/parallel/test-http-parser.js
index 2bf6271e2dd..03ad528c387 100644
--- a/test/parallel/test-http-parser.js
+++ b/test/parallel/test-http-parser.js
@@ -1,3 +1,5 @@
+// Flags: --expose-internals --no-warnings
+
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
@@ -23,7 +25,7 @@
const { mustCall, mustNotCall, mustCallAtLeast } = require('../common');
const assert = require('assert');
-const { methods, HTTPParser } = require('_http_common');
+const { methods, HTTPParser } = require('internal/http/common');
const { REQUEST, RESPONSE } = HTTPParser;
const kOnHeaders = HTTPParser.kOnHeaders | 0;
diff --git a/test/parallel/test-http-server-async-dispose.js b/test/parallel/test-http-server-async-dispose.js
index 8af11dcb7b0..29abde5bb8a 100644
--- a/test/parallel/test-http-server-async-dispose.js
+++ b/test/parallel/test-http-server-async-dispose.js
@@ -1,8 +1,10 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
-const { kConnectionsCheckingInterval } = require('_http_server');
+const { kConnectionsCheckingInterval } = require('internal/http/server');
const server = createServer();
diff --git a/test/parallel/test-http-server-clear-timer.js b/test/parallel/test-http-server-clear-timer.js
index 00a7b9f73e5..78e7208c57f 100644
--- a/test/parallel/test-http-server-clear-timer.js
+++ b/test/parallel/test-http-server-clear-timer.js
@@ -1,8 +1,10 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');
-const { kConnectionsCheckingInterval } = require('_http_server');
+const { kConnectionsCheckingInterval } = require('internal/http/server');
let i = 0;
let timer;
diff --git a/test/parallel/test-http-server-close-destroy-timeout.js b/test/parallel/test-http-server-close-destroy-timeout.js
index b1138ee36d5..b32d5fcdd6b 100644
--- a/test/parallel/test-http-server-close-destroy-timeout.js
+++ b/test/parallel/test-http-server-close-destroy-timeout.js
@@ -1,8 +1,11 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
+
const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
-const { kConnectionsCheckingInterval } = require('_http_server');
+const { kConnectionsCheckingInterval } = require('internal/http/server');
const server = createServer(function(req, res) {});
server.listen(0, common.mustCall(function() {
diff --git a/test/parallel/test-http-server-options-highwatermark.js b/test/parallel/test-http-server-options-highwatermark.js
index 9082550679d..f317d14810f 100644
--- a/test/parallel/test-http-server-options-highwatermark.js
+++ b/test/parallel/test-http-server-options-highwatermark.js
@@ -3,7 +3,7 @@
const common = require('../common');
const assert = require('assert');
const http = require('http');
-const { kHighWaterMark } = require('_http_outgoing');
+const { kHighWaterMark } = require('internal/http/outgoing');
const { getDefaultHighWaterMark } = require('internal/streams/state');
diff --git a/test/parallel/test-http-set-max-idle-http-parser.js b/test/parallel/test-http-set-max-idle-http-parser.js
index d935823a1ba..1d196174304 100644
--- a/test/parallel/test-http-set-max-idle-http-parser.js
+++ b/test/parallel/test-http-set-max-idle-http-parser.js
@@ -1,7 +1,9 @@
+// Flags: --expose-internals --no-warnings
+
'use strict';
require('../common');
const assert = require('assert');
-const httpCommon = require('_http_common');
+const httpCommon = require('internal/http/common');
const http = require('http');
[Symbol(), {}, [], () => {}, 1n, true, '1', null, undefined].forEach((value) => {
diff --git a/test/parallel/test-https-server-async-dispose.js b/test/parallel/test-https-server-async-dispose.js
index 93b5cb74bbd..1cb3748a028 100644
--- a/test/parallel/test-https-server-async-dispose.js
+++ b/test/parallel/test-https-server-async-dispose.js
@@ -1,3 +1,4 @@
+// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
@@ -7,7 +8,7 @@ if (!common.hasCrypto)
const assert = require('assert');
const { createServer } = require('https');
-const { kConnectionsCheckingInterval } = require('_http_server');
+const { kConnectionsCheckingInterval } = require('internal/http/server');
const server = createServer();
diff --git a/test/parallel/test-https-server-close-destroy-timeout.js b/test/parallel/test-https-server-close-destroy-timeout.js
index e876721f610..a99e6f837b7 100644
--- a/test/parallel/test-https-server-close-destroy-timeout.js
+++ b/test/parallel/test-https-server-close-destroy-timeout.js
@@ -1,3 +1,4 @@
+// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
const assert = require('assert');
@@ -6,7 +7,7 @@ if (!common.hasCrypto) {
}
const { createServer } = require('https');
-const { kConnectionsCheckingInterval } = require('_http_server');
+const { kConnectionsCheckingInterval } = require('internal/http/server');
const fixtures = require('../common/fixtures');
diff --git a/test/parallel/test-outgoing-message-pipe.js b/test/parallel/test-outgoing-message-pipe.js
index 049b41c9aaa..8b4632ebeeb 100644
--- a/test/parallel/test-outgoing-message-pipe.js
+++ b/test/parallel/test-outgoing-message-pipe.js
@@ -1,7 +1,7 @@
'use strict';
require('../common');
const assert = require('assert');
-const OutgoingMessage = require('_http_outgoing').OutgoingMessage;
+const OutgoingMessage = require('http').OutgoingMessage;
// Verify that an error is thrown upon a call to `OutgoingMessage.pipe`.
diff --git a/test/parallel/test-warn-http-agent-deprecation.js b/test/parallel/test-warn-http-agent-deprecation.js
new file mode 100644
index 00000000000..3a9456ad7f0
--- /dev/null
+++ b/test/parallel/test-warn-http-agent-deprecation.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const common = require('../common');
+
+// _http_agent is deprecated.
+
+common.expectWarning('DeprecationWarning',
+ 'The _http_agent module is deprecated. Use `node:http` instead.', 'DEP0199');
+
+require('_http_agent');
diff --git a/test/parallel/test-warn-http-client-deprecation.js b/test/parallel/test-warn-http-client-deprecation.js
new file mode 100644
index 00000000000..90bb02df8c7
--- /dev/null
+++ b/test/parallel/test-warn-http-client-deprecation.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const common = require('../common');
+
+// _http_client is deprecated.
+
+common.expectWarning('DeprecationWarning',
+ 'The _http_client module is deprecated. Use `node:http` instead.', 'DEP0199');
+
+require('_http_client');
diff --git a/test/parallel/test-warn-http-common-deprecation.js b/test/parallel/test-warn-http-common-deprecation.js
new file mode 100644
index 00000000000..9e0e18e4919
--- /dev/null
+++ b/test/parallel/test-warn-http-common-deprecation.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const common = require('../common');
+
+// _http_common is deprecated.
+
+common.expectWarning('DeprecationWarning',
+ 'The _http_common module is deprecated.', 'DEP0199');
+
+require('_http_common');
diff --git a/test/parallel/test-warn-http-incoming-deprecation.js b/test/parallel/test-warn-http-incoming-deprecation.js
new file mode 100644
index 00000000000..3b96ea9990b
--- /dev/null
+++ b/test/parallel/test-warn-http-incoming-deprecation.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const common = require('../common');
+
+// _http_incoming is deprecated.
+
+common.expectWarning('DeprecationWarning',
+ 'The _http_incoming module is deprecated. Use `node:http` instead.', 'DEP0199');
+
+require('_http_incoming');
diff --git a/test/parallel/test-warn-http-outgoing-deprecation.js b/test/parallel/test-warn-http-outgoing-deprecation.js
new file mode 100644
index 00000000000..7af44ee92b9
--- /dev/null
+++ b/test/parallel/test-warn-http-outgoing-deprecation.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const common = require('../common');
+
+// _http_outgoing is deprecated.
+
+common.expectWarning('DeprecationWarning',
+ 'The _http_outgoing module is deprecated. Use `node:http` instead.', 'DEP0199');
+
+require('_http_outgoing');
diff --git a/test/parallel/test-warn-http-server-deprecation.js b/test/parallel/test-warn-http-server-deprecation.js
new file mode 100644
index 00000000000..db14bc5ba3c
--- /dev/null
+++ b/test/parallel/test-warn-http-server-deprecation.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const common = require('../common');
+
+// _http_server is deprecated.
+
+common.expectWarning('DeprecationWarning',
+ 'The _http_server module is deprecated. Use `node:http` instead.', 'DEP0199');
+
+require('_http_server');
diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js
index a2ec09775ce..2cb15e144ee 100644
--- a/test/sequential/test-async-wrap-getasyncid.js
+++ b/test/sequential/test-async-wrap-getasyncid.js
@@ -183,7 +183,7 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check
{
- const { HTTPParser } = require('_http_common');
+ const { HTTPParser } = require('internal/http/common');
const parser = new HTTPParser();
testUninitialized(parser, 'HTTPParser');
parser.initialize(HTTPParser.REQUEST, {});
diff --git a/test/sequential/test-http-regr-gh-2928.js b/test/sequential/test-http-regr-gh-2928.js
index f6a9e160328..aa0f15167b6 100644
--- a/test/sequential/test-http-regr-gh-2928.js
+++ b/test/sequential/test-http-regr-gh-2928.js
@@ -1,11 +1,12 @@
+// Flags: --expose-internals --no-warnings
// This test is designed to fail with a segmentation fault in Node.js 4.1.0 and
// execute without issues in Node.js 4.1.1 and up.
'use strict';
const common = require('../common');
const assert = require('assert');
-const httpCommon = require('_http_common');
-const { HTTPParser } = require('_http_common');
+const httpCommon = require('internal/http/common');
+const { HTTPParser } = require('internal/http/common');
const net = require('net');
httpCommon.parsers.max = 50;