koala73/worldmonitor · error

Cyber threats unavailable

Error message

Cyber threats unavailable

What it means

fetchCyberThreats throws 'Cyber threats unavailable' inside the breaker execute callback when the ListCyberThreats RPC response fails the isCyberThreatSnapshot structural check (src/services/cyber/index.ts:112). The snapshot guard validates the response actually contains a threats collection shaped per the shared contract; a default/empty proto response or malformed payload is rejected so the breaker records a failure and serves the empty fallback.

Solutions

  1. Confirm the cyber threats worker produces a valid snapshot and that hydrating 'cyberThreats' succeeds (check the bootstrap key and CDN URL).
  2. Call listCyberThreats manually against getRpcBaseUrl() with the same request (pageSize, start/end window) and inspect whether threats array is populated.
  3. Regenerate clients from proto (make generate) and re-verify isCyberThreatSnapshot in shared/cyber-threat-snapshot matches the current response shape.
  4. Check breaker state: after repeated failures it may open and short-circuit to emptyFallback — reset/reload after the producer is fixed.
  5. Verify the cyber layer enable gate: fetchCyberThreats is on the on-demand path; ensure the panel actually passed the gate and hydration was attempted with the correct key.

Example fix

// before
const response = await client.listCyberThreats({ ... });
if (!isCyberThreatSnapshot(response)) throw new Error('Cyber threats unavailable');
return response;
// after
const response = await client.listCyberThreats({ ... });
if (!isCyberThreatSnapshot(response)) {
  console.warn('[cyber] non-snapshot response:', response);
  throw new Error('Cyber threats unavailable');
}
return response;
Defensive patterns

Strategy: try-catch

Validate before calling

const hydrated = await ensureHydrated('cyberThreats');
if (!isCyberThreatSnapshot(hydrated)) {
  console.warn('cyber bootstrap snapshot invalid; RPC path may also fail');
}

Type guard

// reuse the shared contract guard
import { isCyberThreatSnapshot } from '@/shared/cyber-threat-snapshot';
const ok = isCyberThreatSnapshot(response); // boolean narrowing to ListCyberThreatsResponse

Try / catch

try {
  const threats = await fetchCyberThreats({ limit: 500 });
  renderThreats(threats);
} catch (err) {
  if (err.message === 'Cyber threats unavailable') renderCyberLayerOffline();
  else throw err;
}

Prevention

When it happens

Trigger: client.listCyberThreats resolves with a response lacking a valid threats array (e.g. empty default proto message), or the ensureHydrated('cyberThreats') bootstrap snapshot was invalid so execution fell through to the RPC and the RPC also returned a non-snapshot payload.

Common situations: Cyber data worker not running or its Redis/CDN snapshot empty on a fresh deployment; the CDN-shielded per-key bootstrap fetch (cyberThreats) fails hydration and the RPC endpoint also returns an empty page; proto regeneration drift making the response fail the shared snapshot type guard; wrong RPC base URL in the environment.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22). Data as JSON: /api/errors/9f5e3a2e33b16bc7. Report an issue: GitHub.

Appendix: source

Thrown at src/services/cyber/index.ts:112

    breaker.recordSuccess({ threats: hydrated.threats }, AVAILABLE_CACHE_KEY);
    return hydrated.threats.map(toCyberThreat);
  }

  const limit = clampInt(options.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
  const days = clampInt(options.days, DEFAULT_DAYS, 1, MAX_DAYS);
  const now = Date.now();

  const resp = await breaker.execute(async () => {
    const response = await client.listCyberThreats({
      start: now - days * 24 * 60 * 60 * 1000,
      end: now,
      pageSize: limit,
      cursor: '',
      type: 'CYBER_THREAT_TYPE_UNSPECIFIED',
      source: 'CYBER_THREAT_SOURCE_UNSPECIFIED',
      minSeverity: 'CRITICALITY_LEVEL_UNSPECIFIED',
    });
    if (!isCyberThreatSnapshot(response)) throw new Error('Cyber threats unavailable');
    return response;
  }, emptyFallback, { cacheKey: AVAILABLE_CACHE_KEY, shouldCache: isCyberThreatSnapshot });

  if (breaker.getDataState().mode === 'unavailable') throw new Error('Cyber threats unavailable');
  return resp.threats.map(toCyberThreat);
}

View on GitHub (pinned to e586b8b4b8)