fix: Distributed API proxy memory leak, node switcher refresh, and copy button

Proxy memory leak (MaxListenersExceededWarning + DEP0060):
createProxyMiddleware was instantiated inside the request handler on every
single API call. Each new instance registered fresh 'close' listeners on the
HTTP server and re-ran the http-proxy util._extend deprecated path. After ~10
requests the MaxListeners threshold was breached. Fix: declare ONE global
remoteNodeProxy at startup using the router option to dynamically resolve the
target URL per request. Listeners are registered once. ECONNREFUSED errors are
caught in the on.error handler and returned as structured 502 JSON.

Node switcher "nothing happens":
EditorLayout had a single useEffect([], []) that called refreshStacks() once
on mount. Changing the active node updated NodeContext state and localStorage
but nothing re-triggered the stack list fetch. Fix: split into two effects —
notifications polling (no dependency) and a stack-refresh effect keyed on
activeNode?.id. When the node changes, stale editor/container/file state is
cleared and the stacks for the new node are fetched.

Copy button silently failing:
navigator.clipboard.writeText() throws DOMException in non-HTTPS / non-localhost
contexts (e.g. http://192.168.x.x). The uncaught async exception silently
swallowed the success toast and state update. Fix: wrapped in try/catch with
an execCommand('copy') textarea fallback and a final error toast if both fail.
This commit is contained in:
SaelixCode
2026-03-19 15:34:04 -04:00
parent 45a642014f
commit fddd855624
4 changed files with 83 additions and 25 deletions
+35 -19
View File
@@ -305,7 +305,40 @@ app.use('/api', (req: Request, res: Response, next: NextFunction): void => {
authMiddleware(req, res, next);
});
// Remote Node HTTP Proxy
// Remote Node HTTP Proxy — single global instance.
// Previously, createProxyMiddleware was called inside the request handler on every API
// call, spawning a new proxy instance (and http-proxy server) each time. This caused:
// - MaxListenersExceededWarning: repeated 'close' listeners added to [Server]
// - DEP0060: util._extend called on every http-proxy initialisation
// Fix: create ONE instance at startup; use the router option to resolve the
// target URL dynamically per request without constructing new listeners.
const remoteNodeProxy = createProxyMiddleware<Request, Response>({
target: 'http://localhost:0', // placeholder — overridden per-request by router
changeOrigin: true,
router: (req) => {
const node = NodeRegistry.getInstance().getNode(req.nodeId);
return node?.api_url?.replace(/\/$/, '');
},
on: {
proxyReq: (proxyReq, req) => {
const node = NodeRegistry.getInstance().getNode(req.nodeId);
// Remote Sencho sees itself as local — strip node context and inject bearer auth
proxyReq.removeHeader('x-node-id');
if (node?.api_token) {
proxyReq.setHeader('Authorization', `Bearer ${node.api_token}`);
}
},
error: (err, _req, proxyRes) => {
console.error('[Proxy] Remote node error:', (err as Error).message);
if (!(proxyRes as Response).headersSent) {
(proxyRes as Response).status(502).json({
error: 'Remote node is unreachable. Check the API URL and ensure Sencho is running on that host.'
});
}
},
},
});
// Intercepts all /api/ requests for remote Distributed API nodes and forwards them
// to the target Sencho instance. Node management and auth routes always execute locally.
app.use('/api/', (req: Request, res: Response, next: NextFunction): void => {
@@ -327,24 +360,7 @@ app.use('/api/', (req: Request, res: Response, next: NextFunction): void => {
return;
}
createProxyMiddleware<Request, Response>({
target: node.api_url.replace(/\/$/, ''),
changeOrigin: true,
on: {
proxyReq: (proxyReq) => {
// Remote Sencho is always "local" to itself — strip node context
proxyReq.removeHeader('x-node-id');
proxyReq.setHeader('Authorization', `Bearer ${node.api_token!}`);
},
error: (_err, _req, proxyRes) => {
if (!(proxyRes as Response).headersSent) {
(proxyRes as Response).status(502).json({
error: `Remote node "${node.name}" is unreachable. Check the API URL and ensure Sencho is running on that host.`
});
}
},
},
})(req, res, next);
remoteNodeProxy(req, res, next);
});
// Create HTTP server for WebSocket upgrade handling