Merge pull request #36 from AnsoCode/fix/remote-proxy-cookie-strip-and-middleware-loop

fix: strip browser cookie from proxy requests; fix nodeContextMiddleware self-heal loop
This commit is contained in:
Anso
2026-03-19 18:14:27 -04:00
committed by GitHub
2 changed files with 17 additions and 3 deletions
+2
View File
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- **Fixed:** Remote node proxy forwarding the browser's `sencho_token` cookie to the remote Sencho instance — the remote's `authMiddleware` evaluates `cookieToken || bearerToken` and the cookie (signed with the local JWT secret) was validated before the valid Bearer token, causing 401 on all proxied API calls. Fixed by stripping the `cookie` header in `proxyReq` so only the Bearer token is used for remote authentication.
- **Fixed:** `nodeContextMiddleware` blocking `/api/nodes` when `x-node-id` references a deleted/non-existent node — the nodes list endpoint must always succeed so the frontend can re-sync a stale node ID in localStorage; exempted alongside `/api/auth/`.
- **Fixed:** Remote node proxy stripping the `/api` path prefix — `remoteNodeProxy` is mounted at `app.use('/api/', ...)` so Express strips that prefix from `req.url` before `http-proxy-middleware` sees it; added `pathRewrite: (path) => '/api' + path` to restore the full path when forwarding to the remote Sencho instance (e.g. `/stats``/api/stats`). This was the root cause of all remote API calls returning the remote's SPA HTML instead of JSON.
- **Fixed:** Dashboard cards (Active Containers, Host CPU, Host RAM, Docker Network) showing stale local-node data after switching to a remote node — `HomeDashboard` polling effects now depend on `activeNode?.id` and clear state immediately on node change.
- **Fixed:** `refreshStacks` crashing with `SyntaxError` or `TypeError` when the remote proxy returns a non-JSON response (e.g., connection refused to unreachable remote node) — now checks `res.ok` before calling `res.json()` and iterates a typed `fileList` instead of the raw parsed value.
+15 -3
View File
@@ -82,8 +82,15 @@ const nodeContextMiddleware = (req: Request, res: Response, next: NextFunction)
req.nodeId = NodeRegistry.getInstance().getDefaultNodeId();
}
// Intercept requests to deleted nodes to prevent downstream errors
if (req.path.startsWith('/api/') && !req.path.startsWith('/api/auth/')) {
// Intercept requests to deleted nodes to prevent downstream errors.
// /api/nodes is intentionally exempt: it must always be reachable so the
// frontend can re-sync after a node is deleted (otherwise a stale x-node-id
// in localStorage causes an unrecoverable 404 loop).
if (
req.path.startsWith('/api/') &&
!req.path.startsWith('/api/auth/') &&
!req.path.startsWith('/api/nodes')
) {
const node = DatabaseService.getInstance().getNode(req.nodeId);
if (!node) {
res.status(404).json({ error: `Node with id ${req.nodeId} not found or was deleted.` });
@@ -338,8 +345,13 @@ const remoteNodeProxy = createProxyMiddleware<Request, Response>({
on: {
proxyReq: (proxyReq, req) => {
const node = NodeRegistry.getInstance().getNode(req.nodeId);
// Remote Sencho sees itself as local - strip node context and inject bearer auth
// Strip headers that must not reach the remote instance:
// - x-node-id: remote Sencho treats all requests as local
// - cookie: the browser's sencho_token is signed with THIS instance's JWT secret;
// the remote would try to verify it with its own secret and return 401.
// Authentication is handled exclusively via the Bearer token below.
proxyReq.removeHeader('x-node-id');
proxyReq.removeHeader('cookie');
if (node?.api_token) {
proxyReq.setHeader('Authorization', `Bearer ${node.api_token}`);
}