mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 20:39:05 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
|
||||
37
src/js/internal/net/isIP.ts
Normal file
37
src/js/internal/net/isIP.ts
Normal 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,
|
||||
};
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Readable } = require("internal/streams/readable");
|
||||
const Readable = require("internal/streams/readable");
|
||||
|
||||
const {
|
||||
abortedSymbol,
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
2
src/js/private.d.ts
vendored
@@ -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;
|
||||
|
||||
30
test/js/bun/test/parallel/test-require-builtins.ts
Normal file
30
test/js/bun/test/parallel/test-require-builtins.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
111
test/js/node/test/parallel/test-http-agent-maxtotalsockets.js
Normal file
111
test/js/node/test/parallel/test-http-agent-maxtotalsockets.js
Normal 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);
|
||||
27
test/js/node/test/parallel/test-http-client-finished.js
Normal file
27
test/js/node/test/parallel/test-http-client-finished.js
Normal 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();
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@ let messagesComplete = 0;
|
||||
|
||||
function flushPool() {
|
||||
Buffer.allocUnsafe(Buffer.poolSize - 1);
|
||||
Bun.gc(true)
|
||||
globalThis.gc();
|
||||
}
|
||||
|
||||
function demoBug(part1, part2) {
|
||||
|
||||
54
test/js/node/test/parallel/test-http-parser-free.js
Normal file
54
test/js/node/test/parallel/test-http-parser-free.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
}
|
||||
78
test/js/node/test/parallel/test-http-unix-socket.js
Normal file
78
test/js/node/test/parallel/test-http-unix-socket.js
Normal 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();
|
||||
|
||||
}));
|
||||
@@ -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();
|
||||
});
|
||||
@@ -154,4 +154,4 @@ process.on('exit', function() {
|
||||
console.error(responses);
|
||||
|
||||
assert.strictEqual(responses.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,4 +44,4 @@ server.listen(0, common.mustCall(() => {
|
||||
});
|
||||
res.on('end', () => server.close());
|
||||
});
|
||||
}));
|
||||
}));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user