Summary
The match-events WebSocket gateway writes its dedup-cache entry before running the event processor, and does not catch a processor rejection. A transient failure while processing any event (for example a Hasura hiccup) silently drops that event: the game server gets its ack back, and the retry is suppressed by the dedup entry for the 10-second TTL.
Location
api src/matches/match-events.gateway.ts:114-136 (handleMatchEvent):
if (await this.cache.has(cacheKey)) {
return messageId;
}
await this.cache.put(cacheKey, true, 10); // marked processed BEFORE processing
...
processor.setData(matchId, data);
await processor.process(); // if this throws, the throw is unhandled
return messageId;
Impact
Affects every event type routed through this gateway (round ends, score updates, demo/GOTV state, anti-wallhack status, etc.). If processor.process() throws for a message, the cache entry written a few lines earlier means a redelivery of the same messageId within 10s hits the cache.has short-circuit and returns the ack without processing. The event is lost with only whatever the processor itself logged. Because the throw is not caught, it also surfaces as an unhandled rejection in the gateway.
Suggested Fix
Write the dedup entry only after the processor succeeds, and wrap the processing in try/catch so a failure does not both throw and leave a "processed" marker:
if (await this.cache.has(cacheKey)) return messageId;
const Processor = MatchEvents[event as keyof typeof MatchEvents];
if (!Processor) { this.logger.warn("unable to find event handler", event); return messageId; }
const processor = await this.moduleRef.resolve(Processor);
processor.setData(matchId, data);
try {
await processor.process();
await this.cache.put(cacheKey, true, 10);
} catch (error) {
this.logger.error(`event ${event} failed for match ${matchId}: ${error?.message}`, error?.stack);
throw error; // let the sender retry; no dedup entry was written
}
return messageId;
(If the ack semantics require returning messageId even on failure, still gate the cache.put on success so the retry is not suppressed.)
Surfaced during the 2026-07 review/audit pass (pre-existing, unrelated to any in-flight feature).
Summary
The match-events WebSocket gateway writes its dedup-cache entry before running the event processor, and does not catch a processor rejection. A transient failure while processing any event (for example a Hasura hiccup) silently drops that event: the game server gets its ack back, and the retry is suppressed by the dedup entry for the 10-second TTL.
Location
apisrc/matches/match-events.gateway.ts:114-136(handleMatchEvent):Impact
Affects every event type routed through this gateway (round ends, score updates, demo/GOTV state, anti-wallhack status, etc.). If
processor.process()throws for a message, the cache entry written a few lines earlier means a redelivery of the samemessageIdwithin 10s hits thecache.hasshort-circuit and returns the ack without processing. The event is lost with only whatever the processor itself logged. Because the throw is not caught, it also surfaces as an unhandled rejection in the gateway.Suggested Fix
Write the dedup entry only after the processor succeeds, and wrap the processing in try/catch so a failure does not both throw and leave a "processed" marker:
(If the ack semantics require returning
messageIdeven on failure, still gate thecache.puton success so the retry is not suppressed.)Surfaced during the 2026-07 review/audit pass (pre-existing, unrelated to any in-flight feature).