A refused announcement is said, never fatal (hq issue 217)
The raw subscription that answers discovery ran its loop unguarded, so a refusal escaped as an unhandled rejection and ended the process: every per-module container crash-looped on 2026-10-03 over a subscription that only serves the mesh seeing the runtime. It is now caught, logged, and the runtime serves on. Tested against a bus whose permissions refuse the subject; fails without it.
This commit is contained in:
@@ -361,9 +361,18 @@ export async function connectNats(
|
||||
const sub = conn.subscribe(subject);
|
||||
subs.push(sub);
|
||||
void (async () => {
|
||||
for await (const msg of sub) {
|
||||
const body = answer(msg.subject, msg.data);
|
||||
if (body) msg.respond(body);
|
||||
// **A refusal here is said, never fatal** (novox/hq 04-ISSUES/217). This serves discovery —
|
||||
// what the runtime says about itself — not the work; a bus that refuses it costs the mesh
|
||||
// seeing this runtime, not the runtime's tools and handlers. Unhandled, the refusal ended the
|
||||
// process and every per-module container crash-looped on 2026-10-03.
|
||||
try {
|
||||
for await (const msg of sub) {
|
||||
const body = answer(msg.subject, msg.data);
|
||||
if (body) msg.respond(body);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[mesh-tools] the bus refused ${subject}: ${err instanceof Error ? err.message : String(err)}; ` +
|
||||
"discovery will not see this runtime there, and it serves on");
|
||||
}
|
||||
})();
|
||||
return () => sub.unsubscribe();
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# A bus that refuses one subject: what the mesh's grants do to a subject they do not name (issue 217).
|
||||
authorization {
|
||||
users = [
|
||||
{ user: "runtime", password: "runtime", permissions: {
|
||||
publish: { allow: [">"] }
|
||||
subscribe: { allow: [">"], deny: ["$SRV.PING.>"] }
|
||||
} }
|
||||
]
|
||||
}
|
||||
jetstream: enabled
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* A refused announcement is said and never fatal (novox/hq 04-ISSUES/217): against a bus whose
|
||||
* permissions refuse one discovery subject, the runtime's raw subscription is refused, logged, and
|
||||
* the process keeps serving — its tools still answer.
|
||||
*
|
||||
* docker run -d --rm --name t -p 14233:4222 -v $PWD/test/fixtures/refusing-nats.conf:/c.conf nats:2.10-alpine -c /c.conf
|
||||
* MESH_TEST_REFUSING_NATS=nats://127.0.0.1:14233 node --test --experimental-strip-types test/refused.test.ts
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { connectNats } from "../dist/broker-nats.js";
|
||||
|
||||
const url = process.env.MESH_TEST_REFUSING_NATS;
|
||||
|
||||
test("a refused discovery subscription is logged and the runtime serves on", async (t) => {
|
||||
if (!url) return t.skip("MESH_TEST_REFUSING_NATS unset");
|
||||
const bus = await connectNats({ url, user: "runtime", password: "runtime", module: "alpha", node: "anchor" });
|
||||
const said: string[] = [];
|
||||
const log = console.log;
|
||||
console.log = (...a: unknown[]) => said.push(a.join(" "));
|
||||
const crashed: unknown[] = [];
|
||||
const onRejection = (e: unknown) => crashed.push(e);
|
||||
process.on("unhandledRejection", onRejection);
|
||||
try {
|
||||
(bus as unknown as { raw: (s: string, f: () => Uint8Array | undefined) => () => void }).raw("$SRV.PING.>", () => undefined);
|
||||
const stop = await bus.handle("alpha.ping", async () => ({ pong: true }));
|
||||
for (let i = 0; i < 50 && !said.some((s) => s.includes("the bus refused $SRV.PING.>")); i++) await new Promise((r) => setTimeout(r, 50));
|
||||
console.log = log;
|
||||
assert.ok(said.some((s) => /the bus refused \$SRV\.PING\.>.*serves on/.test(s)), said.join("\n"));
|
||||
assert.equal(crashed.length, 0, `the refusal escaped: ${String(crashed[0])}`);
|
||||
stop();
|
||||
} finally {
|
||||
console.log = log;
|
||||
process.off("unhandledRejection", onRejection);
|
||||
await bus.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user