import { createProxyServer } from "httpxy"; const DEFAULT_PROXY_TIMEOUT_MS = 120_000; const BENIGN_SOCKET_ERRORS = new Set([ "ECONNRESET", "EPIPE", "ECONNABORTED", "ERR_STREAM_PREMATURE_CLOSE", ]); export function matchesPathPrefix(url, prefix) { return ( url === prefix || url.startsWith(prefix + "/") || url.startsWith(prefix + "?") ); } export function createRouter(routes, defaultBackend = null) { const sortedRoutes = Object.entries(routes).sort( ([a], [b]) => b.length - a.length, ); return function route(url) { for (const [prefix, backend] of sortedRoutes) { if (matchesPathPrefix(url, prefix)) { return backend; } } return defaultBackend; }; } export function isBenignSocketError(err) { return Boolean(err && BENIGN_SOCKET_ERRORS.has(err.code)); } function once(fn) { let called = false; return (...args) => { if (called) return; called = true; fn(...args); }; } function writeProxyError(res, message) { if (res.destroyed) return; if (!res.headersSent) { res.writeHead(502, { "Content-Type": "text/plain; charset=utf-8" }); res.end(`Bad Gateway: ${message}`); return; } res.destroy(); } export function createProxyHandlers({ label = "proxy", timeout = DEFAULT_PROXY_TIMEOUT_MS, proxyTimeout = DEFAULT_PROXY_TIMEOUT_MS, } = {}) { const proxy = createProxyServer({ ws: true, changeOrigin: true, xfwd: true, timeout, proxyTimeout, }); const metrics = { activeHttpRequests: 0, activeWebSockets: 0, totalHttpRequests: 0, totalWebSockets: 0, totalErrors: 0, }; proxy.on("error", (err, _req, resOrSocket, target) => { metrics.totalErrors += 1; const targetText = target ? ` -> ${target}` : ""; if (!isBenignSocketError(err)) { console.error(`[${label}] Proxy error${targetText}: ${err.message}`); } if (resOrSocket && typeof resOrSocket.writeHead === "function") { writeProxyError(resOrSocket, err.message); } else if (resOrSocket && typeof resOrSocket.destroy === "function") { resOrSocket.destroy(); } }); function proxyHttp(req, res, target) { metrics.activeHttpRequests += 1; metrics.totalHttpRequests += 1; const finish = once(() => { metrics.activeHttpRequests = Math.max(0, metrics.activeHttpRequests - 1); }); res.on("close", finish); res.on("finish", finish); res.on("error", finish); proxy.web(req, res, { target }).catch((err) => { metrics.totalErrors += 1; if (!isBenignSocketError(err)) { console.error( `[${label}] Proxy error for ${req.url} -> ${target}:`, err, ); } writeProxyError(res, err instanceof Error ? err.message : String(err)); finish(); }); } function proxyWebSocket(req, socket, head, target) { metrics.activeWebSockets += 1; metrics.totalWebSockets += 1; const finish = once(() => { metrics.activeWebSockets = Math.max(0, metrics.activeWebSockets - 1); }); socket.on("close", finish); socket.on("error", finish); try { proxy.ws(req, socket, { target }, head).catch((err) => { metrics.totalErrors += 1; if (!isBenignSocketError(err)) { console.error( `[${label}] WebSocket proxy error for ${req.url} -> ${target}:`, err, ); } socket.destroy(); finish(); }); } catch (err) { metrics.totalErrors += 1; if (!isBenignSocketError(err)) { console.error( `[${label}] WebSocket proxy error for ${req.url} -> ${target}:`, err, ); } socket.destroy(); finish(); } } function dumpMetrics() { console.log( `[${label}] active_http=${metrics.activeHttpRequests} ` + `active_ws=${metrics.activeWebSockets} ` + `total_http=${metrics.totalHttpRequests} ` + `total_ws=${metrics.totalWebSockets} ` + `total_errors=${metrics.totalErrors}`, ); } function installDiagnostics(signal = "SIGUSR1") { process.on(signal, dumpMetrics); return () => { process.off(signal, dumpMetrics); }; } return { proxyHttp, proxyWebSocket, dumpMetrics, installDiagnostics, metrics, }; }