Merge pull request #2075 from PR0M3TH3AN/fix/nostr-count-timeout-12537375718680249854

fix(nostr): prevent count timeouts from disconnecting relays
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-02 15:59:05 -05:00
commit 71e0ecd605
2 files changed
+69 -3

No files matched your search

+12 -3
View File
@@ -8608,9 +8608,18 @@ export class NostrClient {
this.countUnsupportedRelays.add(url);
} else {
logRelayCountFailure(url, error);
this.markRelayUnreachable(url, 60000, {
reason: isTimeout ? "count-timeout" : "count-error",
});
// If the count request timed out, we don't want to kill the entire relay connection
// because it might still be good for subscriptions.
// We only trip the circuit breaker for hard errors.
if (!isTimeout) {
this.markRelayUnreachable(url, 60000, {
reason: "count-error",
});
} else if (isDevMode) {
devLogger.warn(
`[nostr] Relay ${url} count timed out (ignored for circuit breaker).`
);
}
}
return {
url,
@@ -0,0 +1,57 @@
import { test } from "node:test";
import assert from "node:assert";
import { NostrClient } from "../../js/nostr/client.js";
// Mock global objects if needed
if (!globalThis.localStorage) {
globalThis.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
}
test("NostrClient resilience to COUNT timeouts", async (t) => {
const client = new NostrClient();
const relayUrl = "wss://slow.relay.example";
client.relays = [relayUrl];
// Mock the pool
client.pool = {
ensureRelay: async (url) => {
return {
url,
count: async () => {
// Simulate timeout by waiting longer than the client's timeout
await new Promise(r => setTimeout(r, 100));
// In the real code, withRequestTimeout throws the error, the relay just hangs or returns late.
// But here we can simulate the client's wrapper throwing 'count-timeout'
// Actually, client.sendRawCountFrame handles the timeout wrapping.
// So we just need a promise that hangs.
return new Promise(() => {});
}
};
}
};
// We need to override getRequestTimeoutMs to be very short for the test
client.getRequestTimeoutMs = () => 10; // 10ms timeout
// Verify initial state
assert.strictEqual(client.unreachableRelays.has(relayUrl), false);
// Trigger the count
await client.countEventsAcrossRelays([{ kinds: [1] }]);
// Check if relay was marked unreachable
// logic:
// 1. sendRawCountFrame calls withRequestTimeout
// 2. timeouts after 10ms -> throws 'count-timeout'
// 3. countEventsAcrossRelays catches 'count-timeout'
// 4. currently calls markRelayUnreachable
const isUnreachable = client.unreachableRelays.has(relayUrl);
assert.strictEqual(isUnreachable, false, "Relay should NOT be marked unreachable after count timeout");
});