Skip to content

[API] Match-event dedup cache written before processing, silently dropping events on failure #532

Description

@Flegma

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1-highStability & reliabilityaudit-2026-07Findings from the 2026-07 code audit / review passerror-handlingMissing or incorrect error handlingreliabilityReliability or availability concernservice:api5stackgg/api service

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions