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
- Confirm the cyber threats worker produces a valid snapshot and that hydrating 'cyberThreats' succeeds (check the bootstrap key and CDN URL).
- Call listCyberThreats manually against getRpcBaseUrl() with the same request (pageSize, start/end window) and inspect whether threats array is populated.
- Regenerate clients from proto (make generate) and re-verify isCyberThreatSnapshot in shared/cyber-threat-snapshot matches the current response shape.
- Check breaker state: after repeated failures it may open and short-circuit to emptyFallback — reset/reload after the producer is fixed.
- 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
- Keep the cyberThreats bootstrap key fresh on the CDN so hydration succeeds and the RPC path is rarely needed
- Regenerate proto clients after schema changes to keep isCyberThreatSnapshot passing
- Health-check the cyber worker before enabling the cyber layer in the UI
- Log which snapshot predicate fails so producer regressions surface quickly
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
- Cable health unavailable
- Could not resolve to a country.
- country must be an ISO 3166-1 alpha-2 country code, e.g…
- countryCode must be a 2-letter ISO 3166-1 alpha-2 code
- get-intel-timeline: domain Required unless country is set…
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)