mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:08:23 +08:00
* Use libraries for local proxy and static serving * Fix CI for proxy library refactor * Fix static server CI failures Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: openhands <openhands@all-hands.dev>
172 lines
4.2 KiB
JavaScript
172 lines
4.2 KiB
JavaScript
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,
|
|
};
|
|
}
|