DLT / app.js
yousri52's picture
Restore WS compression fix, self-heal, and gossip-host fix
de63df5
Raw
History Blame Contribute Delete
4.19 kB
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const { spawn } = require("child_process");
const app = express();
// http-proxy-middleware (via node http-proxy) doesn't correctly relay
// permessage-deflate compression negotiation for proxied WebSocket upgrades,
// which causes clients to reject compressed frames from the upstream node
// with "Invalid WebSocket frame: RSV1 must be clear". Stripping the
// extension request header prevents compression from being negotiated at
// all, avoiding the mismatch entirely.
function stripWsCompression(proxyReq) {
proxyReq.removeHeader("sec-websocket-extensions");
}
// Ethereum (Anvil) - serves JSON-RPC and WebSocket on the same port
app.use("/eth", createProxyMiddleware({
target: "http://127.0.0.1:8545",
changeOrigin: true,
ws: true,
pathRewrite: { "^/eth": "" },
on: { proxyReqWs: stripWsCompression }
}));
// Rootstock (Anvil) - serves JSON-RPC and WebSocket on the same port
app.use("/rootstock", createProxyMiddleware({
target: "http://127.0.0.1:8546",
changeOrigin: true,
ws: true,
pathRewrite: { "^/rootstock": "" },
on: { proxyReqWs: stripWsCompression }
}));
// Solana - JSON-RPC on 8899, pubsub/WebSocket on a separate port (8900 by
// default), so WS upgrade requests are routed to the pubsub port instead.
app.use("/solana", createProxyMiddleware({
target: "http://127.0.0.1:8899",
changeOrigin: true,
ws: true,
pathRewrite: { "^/solana": "" },
router: (req) => (
req.headers.upgrade && req.headers.upgrade.toLowerCase() === "websocket"
? "http://127.0.0.1:8900"
: "http://127.0.0.1:8899"
),
on: { proxyReqWs: stripWsCompression }
}));
// Per-chain log files written by start.sh
const LOG_FILES = {
eth: "/data/logs/eth.log",
rootstock: "/data/logs/rootstock.log",
solana: "/data/logs/solana.log"
};
app.get("/logs", (req, res) => {
const links = Object.keys(LOG_FILES)
.map((chain) => `<li>
<a href="/logs/${chain}">${chain} (last 200 lines)</a> |
<a href="/logs/${chain}?follow=1">${chain} (live)</a>
</li>`)
.join("\n");
res.type("html").send(`<h1>DLT chain logs</h1><ul>${links}</ul>`);
});
app.get("/logs/:chain", (req, res) => {
const logFile = LOG_FILES[req.params.chain];
if (!logFile) {
return res.status(404).send(`Unknown chain "${req.params.chain}". Valid: ${Object.keys(LOG_FILES).join(", ")}`);
}
const follow = req.query.follow === "1";
const lines = Math.max(1, Math.min(parseInt(req.query.tail, 10) || 200, 5000));
const tailArgs = follow ? ["-F", "-n", String(lines), logFile] : ["-n", String(lines), logFile];
const child = spawn("tail", tailArgs, { stdio: ["ignore", "pipe", "pipe"] });
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("X-Accel-Buffering", "no"); // defensive, in case a buffering proxy sits in front
res.flushHeaders();
child.stdout.pipe(res, { end: false });
child.stderr.pipe(res, { end: false });
let heartbeat = null;
let maxDuration = null;
if (follow) {
heartbeat = setInterval(() => {
res.write(`\n# --- heartbeat ${new Date().toISOString()} ---\n`);
}, 15000);
// Failsafe: never keep a follow session open indefinitely, in case a
// close event is somehow never delivered (e.g. odd proxy behavior).
maxDuration = setTimeout(() => {
res.end("\n# --- session limit reached, reconnect to keep following ---\n");
cleanup();
}, 45 * 60 * 1000);
} else {
// Snapshot mode: end the response once tail exits (EOF on a plain -n read).
child.on("close", () => res.end());
}
function cleanup() {
if (heartbeat) clearInterval(heartbeat);
if (maxDuration) clearTimeout(maxDuration);
if (!child.killed) child.kill("SIGTERM");
}
req.on("close", cleanup);
res.on("close", cleanup);
child.on("error", (err) => {
if (!res.headersSent) res.status(500);
res.end(`\n# error running tail: ${err.message}\n`);
cleanup();
});
});
app.get("/", (req, res) => {
res.send("DLT multi-chain RPC gateway running");
});
app.listen(7860, "0.0.0.0");