From 89ff2238467c0bfc925f7b0d1d99632ee4eed367 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sun, 27 Sep 2026 09:55:48 +0000 Subject: [PATCH] Rudder with no engine running is quiet in dev: the dev backend answers offline instead of 500 dx-shepherd (R500): with no rudder-engine process the dev proxy returned 500 ECONNREFUSED for /api/rudder/{devices,events}, which showed as console errors. While a backend is down, backendDevPlugin now answers its routes itself: /events streams one status event (engine offline, 'The desktop engine is not running.'), which Rudder renders as its own state, and other routes return an empty JSON list. Running engines are proxied as before. --- internal/backend-dev-plugin.mjs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/backend-dev-plugin.mjs b/internal/backend-dev-plugin.mjs index 46993c632..5a7dd82ca 100644 --- a/internal/backend-dev-plugin.mjs +++ b/internal/backend-dev-plugin.mjs @@ -32,10 +32,28 @@ export function devBackendProxy() { export function backendDevPlugin(root) { const children = [] + const alive = new Set() // backends whose process is running right now return { name: 'dx-backend-dev', apply: 'serve', configureServer(server) { + // ENGINE NOT RUNNING, QUIETLY (dx-shepherd, R500): with no backend process the proxy answered + // 500 ECONNREFUSED, which every page showed as console errors. While a backend is down its routes get + // well-formed answers instead: /events streams ONE status event saying the engine is offline (the + // plugin renders its own "engine not running" state from it), anything else returns an empty JSON + // list. Registered here, so it runs before vite's proxy. + for (const b of DEV_BACKENDS) { + server.middlewares.use(b.route, (req, res, next) => { + if (alive.has(b.name)) return next() + if ((req.url || '').startsWith('/events')) { + res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }) + res.write(`data: ${JSON.stringify({ type: 'status', status: { engine: 'offline', error: 'The desktop engine is not running.' } })}\n\n`) + return // held open, so the EventSource does not reconnect in a loop + } + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end('[]') + }) + } for (const b of DEV_BACKENDS) { const bin = resolve(root, b.bin) if (!existsSync(bin)) { @@ -47,8 +65,9 @@ export function backendDevPlugin(root) { const out = (d) => String(d).trim().split('\n').forEach((l) => l && server.config.logger.info(`${tag} ${l}`)) child.stdout.on('data', out) child.stderr.on('data', out) - child.on('exit', (code) => server.config.logger.warn(`${tag} exited (${code})`)) + child.on('exit', (code) => { alive.delete(b.name); server.config.logger.warn(`${tag} exited (${code})`) }) children.push(child) + alive.add(b.name) server.config.logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`) } const stop = () => children.forEach((c) => { try { c.kill('SIGTERM') } catch {} })