{"record":{"id":"04b17bffacebc0aa","repo":"koala73/worldmonitor","slug":"saskalert-active-entry-count-exceeds-max-cap-fetches","errorCode":null,"errorMessage":"saskalert: active entry count exceeds ${MAX_CAP_FETCHES}","messagePattern":"saskalert: active entry count exceeds (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/saskalert.mjs","lineNumber":275,"sourceCode":"    throw new Error(`saskalert: host is not on the allowlist (${SASKALERT_HOST})`);\n  }\n  const fetchFn = opts.fetchFn ?? globalThis.fetch;\n  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n  const maxBytes = opts.maxBytes ?? MAX_PAYLOAD_BYTES;\n  const userAgent = opts.userAgent || CHROME_UA;\n  const nowMs = opts.nowMs ?? Date.now();\n\n  const resp = await fetchFn(url, {\n    headers: { Accept: 'application/json', 'User-Agent': userAgent },\n    signal: AbortSignal.timeout(timeoutMs),\n    redirect: 'error',\n  });\n  if (!resp.ok) throw new Error(`saskalert: HTTP ${resp.status}`);\n  const entries = parseSaskAlertFeed(await readLimitedJson(resp, maxBytes, 'feed'));\n\n  const active = entries.filter((entry) => !isEndedSummaryEntry(entry));\n  if (active.length > MAX_CAP_FETCHES) {\n    throw new Error(`saskalert: active entry count exceeds ${MAX_CAP_FETCHES}`);\n  }\n\n  const verification = { attempted: 0, failed: 0, skippedDeadline: 0, reasons: {} };\n  const unverifiedIds = new Set();\n  const failedLinks = new Set();\n  const alerts = [];\n  const seen = new Set();\n  const seenCapLinks = new Set();\n  const wallStart = opts.nowWallMs ?? Date.now();\n  const budgetMs = opts.capBudgetMs ?? CAP_PHASE_BUDGET_MS;\n  const concurrency = Math.max(1, opts.capConcurrency ?? CAP_CONCURRENCY);\n  let nextIndex = 0;\n\n  async function hydrateOne(entry) {\n    const rememberFailure = (reason) => {\n      unverifiedIds.add(`sk-saskalert-${String(entry.identifier || entry.id || '').trim()}`);\n      verification.reasons[reason] = (verification.reasons[reason] || 0) + 1;\n    };","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/lib/saskalert.mjs#L257-L293","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (active.length > MAX_CAP_FETCHES) {\n  throw new Error(`saskalert: active entry count exceeds ${MAX_CAP_FETCHES}`);\n}\n// after\nif (active.length > MAX_CAP_FETCHES) {\n  console.warn(`saskalert: active entries ${active.length} exceed cap ${MAX_CAP_FETCHES}; hydrating first ${MAX_CAP_FETCHES} by severity`);\n  active.length = MAX_CAP_FETCHES;\n}","handlingStrategy":"validation","validationCode":"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`);","typeGuard":null,"tryCatchPattern":"try {\n  const { alerts } = await fetchSaskAlerts();\n} catch (err) {\n  if (/active entry count exceeds/.test(err?.message)) {\n    console.error('SK feed exceeded CAP fetch cap; check for outbreak or feed schema regression');\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["rate-limit","guard","feed"],"backgroundTag":"value-out-of-range","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}