/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/_http_agent.js
738 строк
21 KB
em
refactor(ext/node): use primordials across the node:* polyfill layer (#34721)
03 июн 2026, 17:24
Не верифицирован
03 июн 2026, 17:24
9935b92
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. import { core, primordials } from "ext:core/mod.js"; import { op_get_env_no_permission_check } from "ext:core/ops"; import * as net from "node:net"; import httpProxy from "node:_http_proxy"; const lazyTls = core.createLazyLoader("node:tls"); const { EventEmitter } = core.loadExtScript("ext:deno_node/_events.mjs"); const { debuglog } = core.loadExtScript( "ext:deno_node/internal/util/debuglog.ts", ); let debug = debuglog("http", (fn) => { debug = fn; }); const { AsyncResource } = core.loadExtScript("ext:deno_node/async_hooks.ts"); const { emitDestroy, emitInit, executionAsyncId, symbols, } = core.loadExtScript("ext:deno_node/internal/async_hooks.ts"); const { async_id_symbol } = symbols; const { once } = core.loadExtScript("ext:deno_node/internal/util.mjs"); const { validateNumber, validateOneOf, validateString, } = core.loadExtScript("ext:deno_node/internal/validators.mjs"); const { ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypePop, ArrayPrototypePush, ArrayPrototypeShift, ArrayPrototypeSome, ArrayPrototypeSplice, FunctionPrototypeCall, NumberIsFinite, NumberParseInt, ObjectCreate, ObjectKeys, ObjectPrototypeIsPrototypeOf, ObjectSetPrototypeOf, ObjectValues, RegExpPrototypeExec, SafeRegExp, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, Symbol, } = primordials; const KEEP_ALIVE_TIMEOUT_RE = new SafeRegExp("^timeout=(\\d+)"); const kOnKeylog = Symbol("onkeylog"); const kRequestOptions = Symbol("requestOptions"); const kRequestAsyncResource = Symbol("requestAsyncResource"); // 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; } } const providerTypeNames = [ "NONE", "DIRHANDLE", "DNSCHANNEL", "ELDHISTOGRAM", "FILEHANDLE", "FILEHANDLECLOSEREQ", "FIXEDSIZEBLOBCOPY", "FSEVENTWRAP", "FSREQCALLBACK", "FSREQPROMISE", "GETADDRINFOREQWRAP", "GETNAMEINFOREQWRAP", "HEAPSNAPSHOT", "HTTP2SESSION", "HTTP2STREAM", "HTTP2PING", "HTTP2SETTINGS", "HTTPINCOMINGMESSAGE", "HTTPCLIENTREQUEST", "JSSTREAM", "JSUDPWRAP", "MESSAGEPORT", "PIPECONNECTWRAP", "PIPESERVERWRAP", "PIPEWRAP", "PROCESSWRAP", "PROMISE", "QUERYWRAP", "SHUTDOWNWRAP", "SIGNALWRAP", "STATWATCHER", "STREAMPIPE", "TCPCONNECTWRAP", "TCPSERVERWRAP", "TCPWRAP", "TLSWRAP", "TTYWRAP", "UDPSENDWRAP", "UDPWRAP", "SIGINTWATCHDOG", "WORKER", "WORKERHEAPSNAPSHOT", "WRITEWRAP", "ZLIB", ]; function freeSocketErrorListener(err) { // deno-lint-ignore no-this-alias const socket = this; debug("SOCKET ERROR on FREE socket:", err.message, err.stack); socket.destroy(); socket.emit("agentRemove"); } export function Agent(options) { if (!ObjectPrototypeIsPrototypeOf(Agent.prototype, this)) { return new Agent(options); } FunctionPrototypeCall(EventEmitter, 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 = ObjectCreate(null); this.sockets = ObjectCreate(null); this.freeSockets = ObjectCreate(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; validateOneOf(this.scheduling, "scheduling", ["fifo", "lifo"]); if (this.maxTotalSockets !== undefined) { validateNumber(this.maxTotalSockets, "maxTotalSockets", 1); } else { this.maxTotalSockets = Infinity; } // Initialize per-agent proxy state from `options.proxyEnv` (if any). // Throws ERR_PROXY_INVALID_CONFIG on malformed URLs. httpProxy.initAgentProxy(this, this.options); 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. // In Node.js, nextTick (where 'free' is emitted) always runs before I/O, // so the socket is guaranteed to be writable here. In Deno, I/O can // interleave with nextTick, so a server FIN may arrive before 'free' // runs, making the socket non-writable but not yet destroyed. if (socket.destroyed) { return; } // For queued requests, require the socket to be writable - don't hand // a half-closed socket to a new request. const requests = this.requests[name]; if (requests && requests.length && !socket.writable) { socket.destroy(); return; } if (requests && requests.length) { const req = ArrayPrototypeShift(requests); 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); ArrayPrototypePush(freeSockets, 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. // deno-lint-ignore no-this-alias const agent = this; this[kOnKeylog] = function onkeylog(keylog) { agent.emit("keylog", keylog, this); }; // Existing sockets will start listening on keylog now. const sockets = ObjectValues(this.sockets); for (let i = 0; i < sockets.length; i++) { sockets[i].on("keylog", this[kOnKeylog]); } } } Agent.defaultMaxSockets = Infinity; // Default connection factory. When a proxy applies to the request, we // connect to the proxy host/port instead of the target. For HTTPS targets // going through an HTTP proxy, this base http.Agent still produces a TCP // socket to the proxy; the https.Agent override builds on top of this to // perform CONNECT-then-TLS tunneling. Agent.prototype.createConnection = function createConnection(options, cb) { const proxy = options && options._proxy; if (proxy && options._proxyProtocol === "http:") { const connectOpts = { __proto__: null, ...options, host: proxy.hostname, hostname: proxy.hostname, port: proxy.port, servername: undefined, }; // Strip target-specific fields that would confuse net.connect. delete connectOpts._proxy; delete connectOpts._proxyTargetHost; delete connectOpts._proxyTargetPort; delete connectOpts._proxyProtocol; if (proxy.protocol === "https:") { connectOpts.servername = proxy.hostname; if ( connectOpts.ca === undefined && op_get_env_no_permission_check("NODE_EXTRA_CA_CERTS") ) { connectOpts.ca = lazyTls().default.getCACertificates("default"); } return lazyTls().default.connect(connectOpts, cb); } return net.createConnection(connectOpts, cb); } return net.createConnection(options, cb); }; // Get the key for a given set of request options Agent.prototype.getName = function getName(options = { __proto__: null }) { 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; }; 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, }; } options = { __proto__: null, ...options, ...this.options }; if (options.socketPath) { options.path = options.socketPath; } if (!options.servername && options.servername !== "") { options.servername = calculateServerName(options, req); } const name = this.getName(options); if (!this.sockets[name]) { this.sockets[name] = []; } const freeSockets = this.freeSockets[name]; let socket; if (freeSockets) { while (freeSockets.length && freeSockets[0].destroyed) { ArrayPrototypeShift(freeSockets); } socket = this.scheduling === "fifo" ? ArrayPrototypeShift(freeSockets) : ArrayPrototypePop(freeSockets); if (!freeSockets.length) { delete this.freeSockets[name]; } } // Reject sockets that are no longer writable (remote end already closed // the connection while the socket was idle in the pool). if (socket && (socket.destroyed || !socket.writable)) { socket.destroy(); socket = null; } const freeLen = freeSockets ? freeSockets.length : 0; const sockLen = freeLen + this.sockets[name].length; if (socket) { asyncResetHandle(socket); this.reuseSocket(socket, req); setRequestSocket(this, req, socket); ArrayPrototypePush(this.sockets[name], socket); } else if ( sockLen < this.maxSockets && this.totalSocketCount < this.maxTotalSockets ) { debug("call onSocket", sockLen, freeLen); // If we are under maxSockets create a new one. this.createSocket(req, options, (err, socket) => { if (err) { req.onSocket(socket, err); } else { setRequestSocket(this, req, socket); } }); } else { debug("wait for socket"); // We are over limit so we'll add it to the queue. if (!this.requests[name]) { 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"); ArrayPrototypePush(this.requests[name], req); } }; Agent.prototype.createSocket = function createSocket(req, options, cb) { options = { __proto__: null, ...options, ...this.options }; if (options.socketPath) { options.path = options.socketPath; } if (!options.servername && options.servername !== "") { options.servername = calculateServerName(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); options.encoding = null; const oncreate = once((err, s) => { if (err) { return cb(err); } if (!this.sockets[name]) { this.sockets[name] = []; } ArrayPrototypePush(this.sockets[name], 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); if (newSocket) { oncreate(null, newSocket); } }; 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 (StringPrototypeStartsWith(hostHeader, "[")) { const index = StringPrototypeIndexOf(hostHeader, "]"); if (index === -1) { // Leading '[', but no ']'. Need to do something... servername = hostHeader; } else { servername = StringPrototypeSlice(hostHeader, 1, index); } } else { servername = StringPrototypeSplit(hostHeader, ":", 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 ( ArrayPrototypeSome( ObjectKeys(sockets), (name) => ArrayPrototypeIncludes(sockets[name], 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) { ArrayPrototypePush(sets, this.freeSockets); } for (let sk = 0; sk < sets.length; sk++) { const sockets = sets[sk]; if (sockets[name]) { const index = ArrayPrototypeIndexOf(sockets[name], s); if (index !== -1) { ArrayPrototypeSplice(sockets[name], index, 1); // Don't leak if (sockets[name].length === 0) { delete sockets[name]; } } } } let req; if (this.requests[name] && 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] && 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) { req.onSocket(socket, err); } else { 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 = RegExpPrototypeExec(KEEP_ALIVE_TIMEOUT_RE, 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); 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") { const oldAsyncId = handle.getAsyncId(); const providerType = handle.getProviderType(); const reusedHandle = new ReusedHandle(providerType, handle); if (oldAsyncId > 0) { emitDestroy(oldAsyncId); } // Assign the handle a new asyncId and run any destroy()/init() hooks. handle.asyncReset(reusedHandle); socket[async_id_symbol] = handle.getAsyncId(); emitInit( socket[async_id_symbol], providerTypeNames[providerType] || "UNKNOWN", executionAsyncId(), reusedHandle, ); } } export let globalAgent = new Agent({ keepAlive: true, scheduling: "lifo", timeout: 5000, }); export function setGlobalAgent(agent) { globalAgent = agent; } export default { Agent, get globalAgent() { return globalAgent; }, set globalAgent(agent) { globalAgent = agent; }, };