koala73/worldmonitor · error
saskalert: active entry count exceeds ${MAX_CAP_FETCHES}
Error message
saskalert: active entry count exceeds ${MAX_CAP_FETCHES} What it means
fetchSaskAlerts throws this when the parsed Saskatchewan alert feed contains more active (non-ended) entries than MAX_CAP_FETCHES, the hard cap on how many CAP documents the fetch phase will hydrate. It is a guard against anomalous feed growth (or a malformed/hostile feed) that would otherwise trigger an unbounded fan-out of per-alert CAP fetches. The error aborts the whole fetch rather than silently truncating alerts.
Solutions
- Raise MAX_CAP_FETCHES in scripts/lib/saskalert.mjs if legitimate Saskatchewan activity genuinely exceeds it, and adjust the CAP-phase budget (capBudgetMs) accordingly.
- Inspect the feed: if ended/expired entries are being counted as active, fix isEndedSummaryEntry to recognize the new ended-marker format.
- Confirm the feed URL is the official SASKALERT_FEED_URL and not a fixture returning an unrealistic entry set.
- If the cap is intentional, treat this as a signal to degrade gracefully: the caller's freshness metadata will show the source never published, so consider pagination or batching of CAP hydration.
- Do not widen the cap without also reviewing MAX_ALERTS, CAP_CONCURRENCY, and CAP_PHASE_BUDGET_MS downstream limits.
Example fix
// before
if (active.length > MAX_CAP_FETCHES) {
throw new Error(`saskalert: active entry count exceeds ${MAX_CAP_FETCHES}`);
}
// after
if (active.length > MAX_CAP_FETCHES) {
console.warn(`saskalert: active entries ${active.length} exceed cap ${MAX_CAP_FETCHES}; hydrating first ${MAX_CAP_FETCHES} by severity`);
active.length = MAX_CAP_FETCHES;
} Defensive patterns
Strategy: validation
Validate before calling
const res = await fetch(feedUrl); const entries = (await res.json()); const active = entries.filter((e) => !isEndedSummaryEntry(e)); if (active.length > MAX_CAP_FETCHES) console.warn(`feed unusually large: ${active.length} active entries`); Try / catch
try {
const { alerts } = await fetchSaskAlerts();
} catch (err) {
if (/active entry count exceeds/.test(err?.message)) {
console.error('SK feed exceeded CAP fetch cap; check for outbreak or feed schema regression');
}
throw err;
} Prevention
- Monitor Saskatchewan alert counts; tune MAX_CAP_FETCHES ahead of severe-weather seasons.
- Add a unit test that runs isEndedSummaryEntry against real feed samples so ended-entry detection survives schema changes.
- Pin the feed to the official SASKALERT_FEED_URL host and reject custom URLs in production.
- Keep MAX_CAP_FETCHES, MAX_ALERTS, and CAP budget constants reviewed together.
When it happens
Trigger: Calling fetchSaskAlerts (directly or via the saskalert seed worker) when entries.filter(!isEndedSummaryEntry).length > MAX_CAP_FETCHES — e.g. a mass-alert day in Saskatchewan producing more concurrent active alerts than the cap, or a feed parse returning junk that all counts as active.
Common situations: Severe weather outbreak (many simultaneous warnings) exceeding the cap constant; upstream feed schema change making ended-entry detection fail so stale entries count as active; a test/staging URL pointing at an oversized fixture.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${d
- Revoke failed (HTTP ${resp.status}).
- Firecrawl extract error: ${data.error ?? 'unknown'}
- P0 scrape failed: HTTP ${resp.status}
- P0 search failed: HTTP ${resp.status}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/04b17bffacebc0aa.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/saskalert.mjs:275
throw new Error(`saskalert: host is not on the allowlist (${SASKALERT_HOST})`);
}
const fetchFn = opts.fetchFn ?? globalThis.fetch;
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const maxBytes = opts.maxBytes ?? MAX_PAYLOAD_BYTES;
const userAgent = opts.userAgent || CHROME_UA;
const nowMs = opts.nowMs ?? Date.now();
const resp = await fetchFn(url, {
headers: { Accept: 'application/json', 'User-Agent': userAgent },
signal: AbortSignal.timeout(timeoutMs),
redirect: 'error',
});
if (!resp.ok) throw new Error(`saskalert: HTTP ${resp.status}`);
const entries = parseSaskAlertFeed(await readLimitedJson(resp, maxBytes, 'feed'));
const active = entries.filter((entry) => !isEndedSummaryEntry(entry));
if (active.length > MAX_CAP_FETCHES) {
throw new Error(`saskalert: active entry count exceeds ${MAX_CAP_FETCHES}`);
}
const verification = { attempted: 0, failed: 0, skippedDeadline: 0, reasons: {} };
const unverifiedIds = new Set();
const failedLinks = new Set();
const alerts = [];
const seen = new Set();
const seenCapLinks = new Set();
const wallStart = opts.nowWallMs ?? Date.now();
const budgetMs = opts.capBudgetMs ?? CAP_PHASE_BUDGET_MS;
const concurrency = Math.max(1, opts.capConcurrency ?? CAP_CONCURRENCY);
let nextIndex = 0;
async function hydrateOne(entry) {
const rememberFailure = (reason) => {
unverifiedIds.add(`sk-saskalert-${String(entry.identifier || entry.id || '').trim()}`);
verification.reasons[reason] = (verification.reasons[reason] || 0) + 1;
};View on GitHub (pinned to 7d06c8633d)