mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
LiveWS server rejects on bind failure instead of crash-looping the process (closes #6324). Integrated into release/v3.8.46.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(live-ws):** the Live Dashboard WebSocket server now **rejects on bind failure** (e.g. `EADDRINUSE` when the API bridge already holds the port) instead of letting the error surface as an unhandled `error` event that crash-loops the process — the `error` listener is attached to `wss` (not `server`) and releases the EventBus subscription on a failed start ([#6324](https://github.com/diegosouzapw/OmniRoute/issues/6324)). Regression guard: `tests/unit/live-ws-eaddrinuse-6324.test.ts`. (thanks @vinayakkulkarni)
|
||||
- **fix(dashboard):** the Home provider-topology widget now trusts the live provider-metrics snapshot — it uses `topology.errorProvider` and live `activeRequests` directly instead of re-deriving state from a stale `lastErrorAt` or applying a frontend timeout filter, so the topology reflects real-time provider health. Regression guard: `tests/unit/home-provider-topology-live-state.test.ts`. (thanks @xz-dev)
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50).
|
||||
|
||||
|
||||
@@ -20,12 +20,7 @@ import { randomUUID } from "crypto";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
import type {
|
||||
WsClientMessage,
|
||||
WsServerMessage,
|
||||
WsEventMessage,
|
||||
WsAuthResult,
|
||||
} from "./types";
|
||||
import type { WsClientMessage, WsServerMessage, WsEventMessage, WsAuthResult } from "./types";
|
||||
|
||||
import { emit, on, onAny, getEventHistory, type HistoryEntry } from "@/lib/events/eventBus";
|
||||
|
||||
@@ -580,7 +575,19 @@ export async function startLiveDashboardServer(
|
||||
clients.clear();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Reject on bind failure (e.g. EADDRINUSE when the API bridge already holds
|
||||
// 20129) instead of crashing the process — the crash-loop of issue #6324.
|
||||
// The listener MUST be on `wss`, not `server`: ws re-emits the server's
|
||||
// "error" via wss.emit(), which throws synchronously when wss has no
|
||||
// listener — before any `server.on("error")` handler could run. The server
|
||||
// never opened, so wss.on("close") won't fire; release the EventBus
|
||||
// subscription here so a failed start leaks nothing.
|
||||
wss.once("error", (err: NodeJS.ErrnoException) => {
|
||||
unsubscribe();
|
||||
clients.clear();
|
||||
reject(err);
|
||||
});
|
||||
server.listen(port, host, () => {
|
||||
console.log("[LiveWS] Dashboard WebSocket server listening on %s:%d", host, port);
|
||||
resolve(server);
|
||||
|
||||
49
tests/unit/live-ws-eaddrinuse-6324.test.ts
Normal file
49
tests/unit/live-ws-eaddrinuse-6324.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test, describe } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import net from "node:net";
|
||||
|
||||
// #6324 regression: startLiveDashboardServer must REJECT on a bind failure
|
||||
// (e.g. EADDRINUSE when the API bridge already holds 20129) rather than let the
|
||||
// error surface as an unhandled 'error' event that crashes the process. The
|
||||
// 3.8.45 standalone bin auto-imports liveServer.ts, whose module-level start
|
||||
// already has a `.catch`; before this fix the listen error never reached it.
|
||||
//
|
||||
// Importing liveServer.ts is safe here: its auto-start guard is short-circuited
|
||||
// by isBuildOrTest() (the node:test runner passes "--test" in process.argv).
|
||||
const { startLiveDashboardServer } = await import("../../src/server/ws/liveServer.ts");
|
||||
|
||||
function occupyPort(host: string): Promise<{ port: number; release: () => Promise<void> }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blocker = net.createServer();
|
||||
blocker.once("error", reject);
|
||||
blocker.listen(0, host, () => {
|
||||
const address = blocker.address();
|
||||
if (address && typeof address === "object") {
|
||||
resolve({
|
||||
port: address.port,
|
||||
release: () => new Promise<void>((r) => blocker.close(() => r())),
|
||||
});
|
||||
} else {
|
||||
blocker.close(() => reject(new Error("Failed to allocate a port")));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("#6324 LiveWS start degrades gracefully on port conflict", () => {
|
||||
test("startLiveDashboardServer rejects with EADDRINUSE when the port is taken", async () => {
|
||||
const host = "127.0.0.1";
|
||||
const { port, release } = await occupyPort(host);
|
||||
try {
|
||||
await assert.rejects(
|
||||
() => startLiveDashboardServer(port, host),
|
||||
(err: NodeJS.ErrnoException) => {
|
||||
assert.equal(err.code, "EADDRINUSE");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user