js: update node:_http_agent (#24275)

pulled out of https://github.com/oven-sh/bun/pull/21809

+7 node tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Meghan Denny
2025-11-04 11:56:33 -08:00
committed by GitHub
parent 4250ce6157
commit fa219a2f8e
30 changed files with 1116 additions and 169 deletions

View File

@@ -11,10 +11,10 @@ const builtin = ${JSON.stringify(builtin)};
const now = performance.now();
require(builtin);
const end = performance.now();
process.stdout.write(JSON.stringify({builtin, time: end - now}) + "\\n");
process.stdout.write(JSON.stringify({ builtin, time: end - now }) + "\\n");
`,
);
const result = spawnSync(typeof Bun !== "undefined" ? "bun" : "node", [path], {
spawnSync(process.execPath, [path], {
stdio: ["inherit", "inherit", "inherit"],
env: {
...process.env,

View File

@@ -202,12 +202,12 @@ if (isBuildkite) {
const doc = await res.json();
console.log(`-> page ${i}, found ${doc.length} items`);
if (doc.length === 0) break;
if (doc.length < per_page) break;
for (const { filename, status } of doc) {
prFileCount += 1;
if (status !== "added") continue;
newFiles.push(filename);
}
if (doc.length < per_page) break;
}
console.log(`- PR ${process.env.BUILDKITE_PULL_REQUEST}, ${prFileCount} files, ${newFiles.length} new files`);
} catch (e) {

View File

@@ -208,6 +208,7 @@ const errors: ErrorCodeMapping = [
["ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT", TypeError, "PostgresError"],
["ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE", TypeError, "PostgresError"],
["ERR_POSTGRES_UNSUPPORTED_NUMERIC_FORMAT", TypeError, "PostgresError"],
["ERR_PROXY_INVALID_CONFIG", Error],
["ERR_MYSQL_CONNECTION_CLOSED", Error, "MySQLError"],
["ERR_MYSQL_CONNECTION_TIMEOUT", Error, "MySQLError"],
["ERR_MYSQL_IDLE_TIMEOUT", Error, "MySQLError"],

View File

@@ -1,3 +1,5 @@
const { isIPv4 } = require("internal/net/isIP");
const {
getHeader,
setHeader,
@@ -360,6 +362,130 @@ const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTT
const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0);
const kOutHeaders = Symbol("kOutHeaders");
function ipToInt(ip) {
const octets = ip.split(".");
let result = 0;
for (let i = 0; i < octets.length; i++) result = (result << 8) + Number.parseInt(octets[i]);
return result >>> 0;
}
class ProxyConfig {
href;
protocol;
auth;
bypassList;
proxyConnectionOptions;
constructor(proxyUrl, keepAlive, noProxyList) {
let parsedURL;
try {
parsedURL = new URL(proxyUrl);
} catch {
throw $ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
}
const { hostname, port, protocol, username, password } = parsedURL;
this.href = proxyUrl;
this.protocol = protocol;
if (username || password) {
// If username or password is provided, prepare the proxy-authorization header.
const auth = `${decodeURIComponent(username)}:${decodeURIComponent(password)}`;
this.auth = `Basic ${Buffer.from(auth).toString("base64")}`;
}
if (noProxyList) {
this.bypassList = noProxyList.split(",").map(entry => entry.trim().toLowerCase());
} else {
this.bypassList = [];
}
this.proxyConnectionOptions = {
// The host name comes from parsed URL so if it starts with '[' it must be an IPv6 address ending with ']'. Remove the brackets for net.connect().
host: hostname[0] === "[" ? hostname.slice(1, -1) : hostname,
// The port comes from parsed URL so it is either '' or a valid number string.
port: port ? Number(port) : protocol === "https:" ? 443 : 80,
};
}
// See: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
shouldUseProxy(hostname, port) {
const bypassList = this.bypassList;
if (this.bypassList.length === 0) return true; // No bypass list, always use the proxy.
const host = hostname.toLowerCase();
const hostWithPort = port ? `${host}:${port}` : host;
for (let i = 0; i < bypassList.length; i++) {
const entry = bypassList[i];
if (entry === "*") return false; // * bypasses all hosts.
if (entry === host || entry === hostWithPort) return false; // Matching host and host:port
// Follow curl's behavior: strip leading dot before matching suffixes.
if (entry.startsWith(".")) {
const suffix = entry.substring(1);
if (host.endsWith(suffix)) return false;
}
// Handle wildcards like *.example.com
if (entry.startsWith("*.") && host.endsWith(entry.substring(1))) return false;
// Handle IP ranges (simple format like 192.168.1.0-192.168.1.255)
// TODO: support IPv6.
if (entry.includes("-") && isIPv4(host)) {
let { 0: startIP, 1: endIP } = entry.split("-");
startIP = startIP.trim();
endIP = endIP.trim();
if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP)) {
const hostInt = ipToInt(host);
const startInt = ipToInt(startIP);
const endInt = ipToInt(endIP);
if (hostInt >= startInt && hostInt <= endInt) return false;
}
}
// It might be useful to support CIDR notation, but it's not so widely supported
// in other tools as a de-facto standard to follow, so we don't implement it for now.
}
return true;
}
}
function parseProxyConfigFromEnv(env, protocol, keepAlive) {
// We only support proxying for HTTP and HTTPS requests.
if (protocol !== "http:" && protocol !== "https:") return null;
// Get the proxy url - following the most popular convention, lower case takes precedence.
// See https://about.gitlab.com/blog/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy
const proxyUrl = protocol === "https:" ? env.https_proxy || env.HTTPS_PROXY : env.http_proxy || env.HTTP_PROXY;
// No proxy settings from the environment, ignore.
if (!proxyUrl) return null;
if (proxyUrl.includes("\r") || proxyUrl.includes("\n")) {
throw $ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
}
// Only http:// and https:// proxies are supported. Ignore instead of throw, in case other protocols are supposed to be handled by the user land.
if (!proxyUrl.startsWith("http://") && !proxyUrl.startsWith("https://")) return null;
return new ProxyConfig(proxyUrl, keepAlive, env.no_proxy || env.NO_PROXY);
}
function checkShouldUseProxy(proxyConfig: ProxyConfig, reqOptions: any) {
if (!proxyConfig) return false;
if (reqOptions.socketPath) return false; // If socketPath is set, the endpoint is a Unix domain socket, which can't be proxied.
return proxyConfig.shouldUseProxy(reqOptions.host || "localhost", reqOptions.port);
}
function filterEnvForProxies(env) {
return {
http_proxy: env.http_proxy,
HTTP_PROXY: env.HTTP_PROXY,
https_proxy: env.https_proxy,
HTTPS_PROXY: env.HTTPS_PROXY,
no_proxy: env.no_proxy,
NO_PROXY: env.NO_PROXY,
};
}
export {
ConnResetException,
Headers,
@@ -369,6 +495,7 @@ export {
assignHeadersFast,
bodyStreamSymbol,
callCloseCallback,
checkShouldUseProxy,
controllerSymbol,
deferredSymbol,
drainMicrotasks,
@@ -378,6 +505,7 @@ export {
emitErrorNextTickIfErrorListenerNT,
eofInProgress,
fakeSocketSymbol,
filterEnvForProxies,
firstWriteSymbol,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
getHeader,
@@ -425,6 +553,7 @@ export {
kUseDefaultPort,
noBodySymbol,
optionsSymbol,
parseProxyConfigFromEnv,
reqSymbol,
runSymbol,
serverSymbol,

View File

@@ -0,0 +1,37 @@
const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";
const v4Str = `(?:${v4Seg}\\.){3}${v4Seg}`;
var IPv4Reg: RegExp | undefined;
const v6Seg = "(?:[0-9a-fA-F]{1,4})";
var IPv6Reg: RegExp | undefined;
function isIPv4(s) {
return (IPv4Reg ??= new RegExp(`^${v4Str}$`)).test(s);
}
function isIPv6(s) {
return (IPv6Reg ??= new RegExp(
"^(?:" +
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
`(?:${v6Seg}:){5}(?::${v4Str}|(?::${v6Seg}){1,2}|:)|` +
`(?:${v6Seg}:){4}(?:(?::${v6Seg}){0,1}:${v4Str}|(?::${v6Seg}){1,3}|:)|` +
`(?:${v6Seg}:){3}(?:(?::${v6Seg}){0,2}:${v4Str}|(?::${v6Seg}){1,4}|:)|` +
`(?:${v6Seg}:){2}(?:(?::${v6Seg}){0,3}:${v4Str}|(?::${v6Seg}){1,5}|:)|` +
`(?:${v6Seg}:){1}(?:(?::${v6Seg}){0,4}:${v4Str}|(?::${v6Seg}){1,6}|:)|` +
`(?::(?:(?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
")(?:%[0-9a-zA-Z-.:]{1,})?$",
)).test(s);
}
function isIP(s) {
if (isIPv4(s)) return 4;
if (isIPv6(s)) return 6;
return 0;
}
export default {
isIPv4,
isIPv6,
isIP,
};

View File

@@ -126,6 +126,17 @@ function once(callback, { preserveReturnValue = false } = kEmptyObject) {
const kEmptyObject = ObjectFreeze(Object.create(null));
function getLazy<T>(initializer: () => T) {
let value: T;
let initialized = false;
return function () {
if (initialized) return value;
value = initializer();
initialized = true;
return value;
};
}
//
export default {
@@ -137,6 +148,7 @@ export default {
NodeAggregateError,
ErrnoException,
once,
getLazy,
kHandle: Symbol("kHandle"),
kAutoDestroyed: Symbol("kAutoDestroyed"),

View File

@@ -1,153 +1,488 @@
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
const EventEmitter = require("node:events");
const { parseProxyConfigFromEnv, kProxyConfig, checkShouldUseProxy, kWaitForProxyTunnel } = require("internal/http");
const { getLazy, kEmptyObject, once } = require("internal/shared");
const { validateNumber, validateOneOf, validateString } = require("internal/validators");
const { isIP } = require("internal/net/isIP");
const { kEmptyObject } = require("internal/http");
const kOnKeylog = Symbol("onkeylog");
const kRequestOptions = Symbol("requestOptions");
const { FakeSocket } = require("internal/http/FakeSocket");
const ObjectDefineProperty = Object.defineProperty;
const kfakeSocket = Symbol("kfakeSocket");
const NODE_HTTP_WARNING =
"WARN: Agent is mostly unused in Bun's implementation of http. If you see strange behavior, this is probably the cause.";
// Define Agent interface
interface Agent extends InstanceType<typeof EventEmitter> {
defaultPort: number;
protocol: string;
options: any;
requests: Record<string, any>;
sockets: Record<string, any>;
freeSockets: Record<string, any>;
keepAliveMsecs: number;
keepAlive: boolean;
maxSockets: number;
maxFreeSockets: number;
scheduling: string;
maxTotalSockets: any;
totalSocketCount: number;
[kfakeSocket]?: any;
createConnection(): any;
getName(options?: any): string;
addRequest(): void;
createSocket(req: any, options: any, cb: (err: any, socket: any) => void): void;
removeSocket(): void;
keepSocketAlive(): boolean;
reuseSocket(): void;
destroy(): void;
function freeSocketErrorListener(err) {
const socket = this;
$debug("SOCKET ERROR on FREE socket:", err.message, err.stack);
socket.destroy();
socket.emit("agentRemove");
}
// Define the constructor interface
interface AgentConstructor {
new (options?: any): Agent;
(options?: any): Agent;
defaultMaxSockets: number;
globalAgent: Agent;
prototype: Agent;
}
function Agent(options = kEmptyObject) {
type Agent = import("node:http").Agent;
function Agent(options): void {
if (!(this instanceof Agent)) return new Agent(options);
EventEmitter.$apply(this, []);
EventEmitter.$call(this);
this.defaultPort = 80;
this.protocol = "http:";
this.options = { __proto__: null, ...options };
this.options = options = { ...options, path: null };
if (options.noDelay === undefined) options.noDelay = true;
this.defaultPort = this.options.defaultPort || 80;
this.protocol = this.options.protocol || "http:";
// Don't confuse net and make it think that we're connecting to a pipe
this.requests = Object.create(null);
this.sockets = Object.create(null);
this.freeSockets = Object.create(null);
if (this.options.noDelay === undefined) this.options.noDelay = true;
this.keepAliveMsecs = options.keepAliveMsecs || 1000;
this.keepAlive = options.keepAlive || false;
this.maxSockets = options.maxSockets || Agent.defaultMaxSockets;
this.maxFreeSockets = options.maxFreeSockets || 256;
this.scheduling = options.scheduling || "lifo";
this.maxTotalSockets = options.maxTotalSockets;
// Don't confuse node: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.defaultPort = options.defaultPort || 80;
this.protocol = options.protocol || "http:";
this.agentKeepAliveTimeoutBuffer =
typeof this.options.agentKeepAliveTimeoutBuffer === "number" &&
this.options.agentKeepAliveTimeoutBuffer >= 0 &&
Number.isFinite(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: 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();
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._httpMessage = null;
this.removeSocket(socket, options);
socket.once("error", freeSocketErrorListener);
freeSockets.push(socket);
});
// Don't emit keylog events unless there is a listener for them.
this.on("newListener", maybeEnableKeylog);
}
$toClass(Agent, "Agent", EventEmitter);
// Type assertion to help TypeScript understand Agent has static properties
const AgentClass = Agent as unknown as AgentConstructor;
function maybeEnableKeylog(this: Agent, 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.
const sockets = Object.values(this.sockets);
for (let i = 0; i < sockets.length; i++) {
sockets[i]!.on("keylog", this[kOnKeylog]);
}
}
}
ObjectDefineProperty(AgentClass, "globalAgent", {
get: function () {
return globalAgent;
},
});
const tls = getLazy(() => require("node:tls"));
const net = getLazy(() => require("node:net"));
ObjectDefineProperty(AgentClass, "defaultMaxSockets", {
get: function () {
return Infinity;
},
});
Agent.defaultMaxSockets = Infinity;
Agent.prototype.createConnection = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.createConnection is a no-op, returns fake socket");
return (this[kfakeSocket] ??= new FakeSocket());
Agent.prototype.createConnection = function createConnection(...args) {
const normalized = net()._normalizeArgs(args);
const options = normalized[0];
const cb = normalized[1];
const shouldUseProxy = checkShouldUseProxy(this[kProxyConfig], options);
$debug(`http createConnection should use proxy for ${options.host}:${options.port}:`, shouldUseProxy);
if (!shouldUseProxy) {
// @ts-ignore
return net().createConnection(...args);
}
const connectOptions = {
...this[kProxyConfig].proxyConnectionOptions,
};
const proxyProtocol = this[kProxyConfig].protocol;
if (proxyProtocol === "http:") {
// @ts-ignore
return net().connect(connectOptions, cb);
} else if (proxyProtocol === "https:") {
// @ts-ignore
return tls().connect(connectOptions, cb);
}
// This should be unreachable because proxy config should be null for other protocols.
$assert(false, `Unexpected proxy protocol ${proxyProtocol}`);
};
Agent.prototype.getName = function (options = kEmptyObject) {
Agent.prototype.getName = function getName(options = kEmptyObject) {
let name = options.host || "localhost";
name += ":";
if (options.port) {
name += options.port;
}
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}`;
}
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 () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.addRequest is a no-op");
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.
} else {
req.emit("error", err);
}
}
}
Agent.prototype.addRequest = function addRequest(req, options, port /* legacy */, localAddress /* legacy */) {
$debug("WARN: Agent.addRequest is a no-op");
return; // TODO:
// 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;
if (socket) {
this.reuseSocket(socket, req);
setRequestSocket(this, req, socket);
this.sockets[name].push(socket);
} else if (sockLen < this.maxSockets && this.totalSocketCount < this.maxTotalSockets) {
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");
this.requests[name] ||= [];
req[kRequestOptions] = options;
this.requests[name].push(req);
}
};
Agent.prototype.createSocket = function (req, options, cb) {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.createSocket returns fake socket");
cb(null, (this[kfakeSocket] ??= new FakeSocket()));
Agent.prototype.createSocket = function createSocket(req, options, cb) {
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);
});
if (this.keepAlive) {
options.keepAlive = this.keepAlive;
options.keepAliveInitialDelay = this.keepAliveMsecs;
}
const newSocket = this.createConnection(options, oncreate);
if (newSocket && !newSocket[kWaitForProxyTunnel]) oncreate(null, newSocket);
};
Agent.prototype.removeSocket = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.removeSocket is a no-op");
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 (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() {
$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");
const sockets = agent.freeSockets;
if (Object.keys(sockets).some(name => sockets[name].includes(s))) {
return s.destroy();
}
}
s.on("timeout", onTimeout);
function onRemove() {
$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 (!s.writable) sets.push(this.freeSockets);
for (let sk = 0; sk < sets.length; sk++) {
const sockets = sets[sk];
const socket = sockets[name];
if (socket) {
const index = socket.indexOf(s);
if (index !== -1) {
socket.splice(index, 1);
if (socket.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 {
const keys = Object.keys(this.requests);
for (let i = 0; i < keys.length; i++) {
const prop = keys[i];
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;
this.createSocket(req, options, (err, socket) => {
if (err) {
handleSocketAfterProxy(err, req);
req.onSocket(null, err);
return;
}
socket.emit("free");
});
}
};
Agent.prototype.keepSocketAlive = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.keepSocketAlive is a no-op");
return true;
Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setKeepAlive(true, this.keepAliveMsecs);
socket.unref();
let agentTimeout = this.options.timeout || 0;
let canKeepSocketAlive = true;
const res = socket._httpMessage?.res;
if (res) {
const keepAliveHint = 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 = Number.parseInt(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 () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.reuseSocket is a no-op");
Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
$debug("have free socket");
socket.removeListener("error", freeSocketErrorListener);
req.reusedSocket = true;
socket.ref();
};
Agent.prototype.destroy = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.destroy is a no-op");
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 = Object.keys(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();
}
}
}
};
var globalAgent = new Agent();
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);
}
const http_agent_exports = {
Agent: AgentClass,
globalAgent,
NODE_HTTP_WARNING,
export default {
Agent,
globalAgent: new Agent({
keepAlive: true,
scheduling: "lifo",
timeout: 5000,
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
// proxyEnv: getOptionValue("--use-env-proxy") ? filterEnvForProxies(process.env) : undefined,
proxyEnv: undefined, // TODO:
}),
};
export default http_agent_exports;

View File

@@ -1,4 +1,4 @@
const { isIP, isIPv6 } = require("node:net");
const { isIP, isIPv6 } = require("internal/net/isIP");
const { checkIsHttpToken, validateFunction, validateInteger, validateBoolean } = require("internal/validators");
const { urlToHttpOptions } = require("internal/url");
@@ -44,7 +44,7 @@ const {
ConnResetException,
} = require("internal/http");
const { Agent, NODE_HTTP_WARNING } = require("node:_http_agent");
const { globalAgent } = require("node:_http_agent");
const { IncomingMessage } = require("node:_http_incoming");
const { OutgoingMessage } = require("node:_http_outgoing");
@@ -639,7 +639,7 @@ function ClientRequest(input, options, cb) {
this[kAbortController] = null;
let agent = options.agent;
const defaultAgent = options._defaultAgent || Agent.globalAgent;
const defaultAgent = options._defaultAgent || globalAgent;
if (agent === false) {
agent = new defaultAgent.constructor();
} else if (agent == null) {

View File

@@ -1,4 +1,4 @@
const { Readable } = require("internal/streams/readable");
const Readable = require("internal/streams/readable");
const {
abortedSymbol,

View File

@@ -53,7 +53,7 @@ const {
validateAbortSignal,
} = require("internal/validators");
const { isIP } = require("node:net");
const { isIP } = require("internal/net/isIP");
const EventEmitter = require("node:events");

View File

@@ -1,7 +1,7 @@
// Hardcoded module "node:dns"
const dns = Bun.dns;
const utilPromisifyCustomSymbol = Symbol.for("nodejs.util.promisify.custom");
const { isIP } = require("node:net");
const { isIP } = require("internal/net/isIP");
const {
validateFunction,
validateArray,

View File

@@ -33,6 +33,7 @@ import type { TLSSocket } from "node:tls";
const { kTimeout, getTimerDuration } = require("internal/timers");
const { validateFunction, validateNumber, validateAbortSignal, validatePort, validateBoolean, validateInt32, validateString } = require("internal/validators"); // prettier-ignore
const { NodeAggregateError, ErrnoException } = require("internal/shared");
const { isIPv4, isIPv6, isIP } = require("internal/net/isIP");
const ArrayPrototypeIncludes = Array.prototype.includes;
const ArrayPrototypePush = Array.prototype.push;
@@ -55,40 +56,6 @@ const upgradeDuplexToTLS = $newZigFunction("socket.zig", "jsUpgradeDuplexToTLS",
const isNamedPipeSocket = $newZigFunction("socket.zig", "jsIsNamedPipeSocket", 1);
const getBufferedAmount = $newZigFunction("socket.zig", "jsGetBufferedAmount", 1);
// IPv4 Segment
const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";
const v4Str = `(?:${v4Seg}\\.){3}${v4Seg}`;
var IPv4Reg;
// IPv6 Segment
const v6Seg = "(?:[0-9a-fA-F]{1,4})";
var IPv6Reg;
function isIPv4(s): boolean {
return (IPv4Reg ??= new RegExp(`^${v4Str}$`)).test(s);
}
function isIPv6(s): boolean {
return (IPv6Reg ??= new RegExp(
"^(?:" +
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
`(?:${v6Seg}:){5}(?::${v4Str}|(?::${v6Seg}){1,2}|:)|` +
`(?:${v6Seg}:){4}(?:(?::${v6Seg}){0,1}:${v4Str}|(?::${v6Seg}){1,3}|:)|` +
`(?:${v6Seg}:){3}(?:(?::${v6Seg}){0,2}:${v4Str}|(?::${v6Seg}){1,4}|:)|` +
`(?:${v6Seg}:){2}(?:(?::${v6Seg}){0,3}:${v4Str}|(?::${v6Seg}){1,5}|:)|` +
`(?:${v6Seg}:){1}(?:(?::${v6Seg}){0,4}:${v4Str}|(?::${v6Seg}){1,6}|:)|` +
`(?::(?:(?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
")(?:%[0-9a-zA-Z-.:]{1,})?$",
)).test(s);
}
function isIP(s): 0 | 4 | 6 {
if (isIPv4(s)) return 4;
if (isIPv6(s)) return 6;
return 0;
}
const bunTlsSymbol = Symbol.for("::buntls::");
const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::");
const owner_symbol = Symbol("owner_symbol");

2
src/js/private.d.ts vendored
View File

@@ -253,7 +253,7 @@ declare function $bindgenFn<T = (...args: any) => any>(filename: string, symbol:
// NOTE: $debug, $assert, and $isPromiseFulfilled omitted
declare module "node:net" {
export function _normalizeArgs(args: any[]): unknown[];
function _normalizeArgs(options: any[]): [Record<PropertyKey, any>, Function | null];
interface Socket {
_handle: Bun.Socket<{ self: Socket; req?: object }> | null;

View File

@@ -0,0 +1,30 @@
import { spawnSync } from "child_process";
import { builtinModules } from "node:module";
import { tempDirWithFiles } from "./../../../../harness";
import { join } from "node:path";
import { expect } from "bun:test";
for (let builtin of builtinModules) {
const safe = builtin.replaceAll("/", "_").replaceAll(":", "_");
const base = safe + ".cjs";
const dir = tempDirWithFiles("", {
[`${base}`]: `
const builtin = ${JSON.stringify(builtin)};
console.log(builtin);
const now = performance.now();
require(builtin);
const end = performance.now();
console.log(JSON.stringify({ builtin, time: end - now }));
`,
});
const path = join(dir, base);
const proc = spawnSync(process.execPath, [path], {
stdio: ["inherit", "inherit", "inherit"],
env: {
...process.env,
NODE_NO_WARNINGS: "1",
},
});
expect(proc.signal).toBeNull();
expect(proc.status).toBe(0);
}

View File

@@ -130,7 +130,7 @@ if (process.argv.length === 2 &&
// If the binary is build without `intl` the inspect option is
// invalid. The test itself should handle this case.
(process.features.inspector || !flag.startsWith('--inspect'))) {
if (flag === "--expose-gc" && process.versions.bun) {
if ((flag === "--expose-gc" || flag === "--expose_gc") && process.versions.bun) {
globalThis.gc ??= () => Bun.gc(true);
break;
}

View File

@@ -0,0 +1,111 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');
assert.throws(() => new http.Agent({
maxTotalSockets: 'test',
}), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "maxTotalSockets" argument must be of type number. ' +
"Received type string ('test')",
});
[-1, 0, NaN].forEach((item) => {
assert.throws(() => new http.Agent({
maxTotalSockets: item,
}), {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
});
});
assert.ok(new http.Agent({
maxTotalSockets: Infinity,
}));
function start(param = {}) {
const { maxTotalSockets, maxSockets } = param;
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxTotalSockets,
maxSockets,
maxFreeSockets: 3
});
const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 6));
const server2 = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 6));
server.keepAliveTimeout = 0;
server2.keepAliveTimeout = 0;
const countdown = new Countdown(12, () => {
assert.strictEqual(getRequestCount(), 0);
agent.destroy();
server.close();
server2.close();
});
function handler(s) {
for (let i = 0; i < 6; i++) {
http.get({
host: 'localhost',
port: s.address().port,
agent,
path: `/${i}`,
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.on('end', common.mustCall(() => {
for (const key of Object.keys(agent.sockets)) {
assert(agent.sockets[key].length <= maxSockets);
}
assert(getTotalSocketsCount() <= maxTotalSockets);
countdown.dec();
}));
}));
}
}
function getTotalSocketsCount() {
let num = 0;
for (const key of Object.keys(agent.sockets)) {
num += agent.sockets[key].length;
}
return num;
}
function getRequestCount() {
let num = 0;
for (const key of Object.keys(agent.requests)) {
num += agent.requests[key].length;
}
return num;
}
server.listen(0, common.mustCall(() => handler(server)));
server2.listen(0, common.mustCall(() => handler(server2)));
}
// If maxTotalSockets is larger than maxSockets,
// then the origin check will be skipped
// when the socket is removed.
[{
maxTotalSockets: 2,
maxSockets: 3,
}, {
maxTotalSockets: 3,
maxSockets: 2,
}, {
maxTotalSockets: 2,
maxSockets: 2,
}].forEach(start);

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const http = require('http');
const { finished } = require('stream');
{
// Test abort before finished.
const server = http.createServer(function(req, res) {
res.write('asd');
});
server.listen(0, common.mustCall(function() {
http.request({
port: this.address().port
})
.on('response', (res) => {
res.on('readable', () => {
res.destroy();
});
finished(res, common.mustCall(() => {
server.close();
}));
})
.end();
}));
}

View File

@@ -0,0 +1,15 @@
'use strict';
const common = require('../common');
if (common.isWindows) return; // TODO: BUN
const http = require('http');
const server = http.createServer((req, res) => res.flushHeaders());
server.listen(common.mustCall(() => {
const req =
http.get({ port: server.address().port }, common.mustCall((res) => {
res.on('timeout', common.mustCall(() => req.destroy()));
res.setTimeout(1);
server.close();
}));
}));

View File

@@ -15,6 +15,7 @@ vals.forEach((v) => {
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "options.hostname" property must be of type string, undefined, or null.' + received
}
);
@@ -23,6 +24,7 @@ vals.forEach((v) => {
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "options.host" property must be of type string, undefined, or null.' + received
}
);
});

View File

@@ -1,7 +1,7 @@
// Flags: --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const { getDefaultHighWaterMark } = require('stream');
const http = require('http');
const OutgoingMessage = http.OutgoingMessage;
@@ -11,7 +11,8 @@ msg._implicitHeader = function() {};
// Writes should be buffered until highwatermark
// even when no socket is assigned.
assert.strictEqual(msg.write('asd'), true);
while (msg.write('asd'));
const highwatermark = msg.writableHighWaterMark;
const highwatermark = msg.writableHighWaterMark || getDefaultHighWaterMark();
assert(msg.outputSize >= highwatermark);

View File

@@ -89,8 +89,7 @@ assert.throws(() => {
}, {
code: "ERR_INVALID_ARG_TYPE",
name: "TypeError",
message:
'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received type number (1)',
message: 'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received type number (1)',
});
assert.throws(() => {

View File

@@ -18,7 +18,7 @@ let messagesComplete = 0;
function flushPool() {
Buffer.allocUnsafe(Buffer.poolSize - 1);
Bun.gc(true)
globalThis.gc();
}
function demoBug(part1, part2) {

View File

@@ -0,0 +1,54 @@
// 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 common = require('../common');
if (common.isMacOS && require('os').release().split(".")[0] === "22") return; // TODO: BUN macOS 13
if (common.isMacOS && process.arch === "arm64" && process.env.CI === "true") return; // TODO: BUN CI
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');
const N = 100;
const server = http.createServer(function(req, res) {
res.end('Hello');
});
const countdown = new Countdown(N, () => server.close());
server.listen(0, function() {
http.globalAgent.maxSockets = 1;
let parser;
for (let i = 0; i < N; ++i) {
(function makeRequest(i) {
const req = http.get({ port: server.address().port }, function(res) {
if (!parser) {
parser = req.parser;
} else {
assert.strictEqual(req.parser, parser);
}
countdown.dec();
res.resume();
});
})(i);
}
});

View File

@@ -12,7 +12,6 @@ const { createConnection } = require('net');
const server = createServer();
server.on('connection', mustCall((socket) => {
socket.on('error', expectsError({
name: 'Error',
message: 'Parse Error: Invalid method encountered',
@@ -38,11 +37,11 @@ server.listen(0, () => {
});
socket.on('end', mustCall(() => {
const expected = Buffer.from(
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
);
assert(Buffer.concat(chunks).equals(expected));
server.close();
}));
});
});

View File

@@ -0,0 +1,86 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
{
const msg = [
'POST / HTTP/1.1',
'Host: 127.0.0.1',
'Transfer-Encoding: chunked',
'Transfer-Encoding: chunked-false',
'Connection: upgrade',
'',
'1',
'A',
'0',
'',
'GET /flag HTTP/1.1',
'Host: 127.0.0.1',
'',
'',
].join('\r\n');
const server = http.createServer(common.mustNotCall((req, res) => {
res.end();
}, 1));
server.listen(0, common.mustSucceed(() => {
const client = net.connect(server.address().port, 'localhost');
let response = '';
// Verify that the server listener is never called
client.on('data', common.mustCall((chunk) => {
response += chunk;
}));
client.setEncoding('utf8');
client.on('error', common.mustNotCall());
client.on('end', common.mustCall(() => {
assert.strictEqual(
response,
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
);
server.close();
}));
client.write(msg);
client.resume();
}));
}
{
const msg = [
'POST / HTTP/1.1',
'Host: 127.0.0.1',
'Transfer-Encoding: chunked',
' , chunked-false',
'Connection: upgrade',
'',
'1',
'A',
'0',
'',
'GET /flag HTTP/1.1',
'Host: 127.0.0.1',
'',
'',
].join('\r\n');
const server = http.createServer(common.mustNotCall());
server.listen(0, common.mustSucceed(() => {
const client = net.connect(server.address().port, 'localhost');
client.on('end', common.mustCall(function() {
server.close();
}));
client.write(msg);
client.resume();
}));
}

View File

@@ -0,0 +1,78 @@
// 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 common = require('../common');
if (common.isWindows) return; // TODO: BUN
const assert = require('assert');
const http = require('http');
const server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Connection': 'close'
});
res.write('hello ');
res.write('world\n');
res.end();
});
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
server.listen(common.PIPE, common.mustCall(function() {
const options = {
socketPath: common.PIPE,
path: '/'
};
const req = http.get(options, common.mustCall(function(res) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers['content-type'], 'text/plain');
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', common.mustCall(function() {
assert.strictEqual(res.body, 'hello world\n');
server.close(common.mustCall(function(error) {
assert.strictEqual(error, undefined);
server.close(common.expectsError({
code: 'ERR_SERVER_NOT_RUNNING',
message: 'Server is not running.',
name: 'Error'
}));
}));
}));
}));
req.on('error', function(e) {
assert.fail(e);
});
req.end();
}));

View File

@@ -0,0 +1,63 @@
// 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 common = require('../common');
if (common.isMacOS && require('os').release().split(".")[0] === "22") return; // TODO: BUN macOS 13
if (common.isMacOS && process.arch === "arm64" && process.env.CI === "true") return; // TODO: BUN CI
if (!common.hasCrypto)
common.skip('missing crypto');
const { readKey } = require('../common/fixtures');
const assert = require('assert');
const https = require('https');
const url = require('url');
// https options
const httpsOptions = {
key: readKey('agent1-key.pem'),
cert: readKey('agent1-cert.pem')
};
function check(request) {
// Assert that I'm https
assert.ok(request.socket._secureEstablished);
}
const server = https.createServer(httpsOptions, function(request, response) {
// Run the check function
check(request);
response.writeHead(200, {});
response.end('ok');
server.close();
});
server.listen(0, function() {
const testURL = url.parse(`https://localhost:${this.address().port}`);
testURL.rejectUnauthorized = false;
// make the request
const clientRequest = https.request(testURL);
// Since there is a little magic with the agent
// make sure that the request uses the https.Agent
assert.ok(clientRequest.agent instanceof https.Agent);
clientRequest.end();
});

View File

@@ -154,4 +154,4 @@ process.on('exit', function() {
console.error(responses);
assert.strictEqual(responses.length, 8);
});
});

View File

@@ -44,4 +44,4 @@ server.listen(0, common.mustCall(() => {
});
res.on('end', () => server.close());
});
}));
}));

View File

@@ -52,6 +52,7 @@ test/js/third_party/astro/astro-post.test.js
test/regression/issue/ctrl-c.test.ts
test/bundler/bundler_comments.test.ts
test/js/node/test/parallel/test-fs-promises-file-handle-readLines.mjs
test/js/bun/test/parallel/test-require-builtins.ts
# trips asan on my macos test machine
test/js/node/test/parallel/test-fs-watch.js