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.
This commit is contained in:
Doxa Maps 2026-09-27 09:55:48 +00:00
parent 374cd5da62
commit 89ff223846

View file

@ -32,10 +32,28 @@ export function devBackendProxy() {
export function backendDevPlugin(root) { export function backendDevPlugin(root) {
const children = [] const children = []
const alive = new Set() // backends whose process is running right now
return { return {
name: 'dx-backend-dev', name: 'dx-backend-dev',
apply: 'serve', apply: 'serve',
configureServer(server) { 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) { for (const b of DEV_BACKENDS) {
const bin = resolve(root, b.bin) const bin = resolve(root, b.bin)
if (!existsSync(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}`)) const out = (d) => String(d).trim().split('\n').forEach((l) => l && server.config.logger.info(`${tag} ${l}`))
child.stdout.on('data', out) child.stdout.on('data', out)
child.stderr.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) children.push(child)
alive.add(b.name)
server.config.logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`) 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 {} }) const stop = () => children.forEach((c) => { try { c.kill('SIGTERM') } catch {} })