koala73/worldmonitor · warning

CAP_VERIFICATION_FAILED

CAP_VERIFICATION_FAILED

Error message

saskalert: incomplete CAP verification; success clock unchanged

What it means

saskAlertBeforePublish throws this tagged error (code CAP_VERIFICATION_FAILED) when the fetched SK snapshot could not fully verify every alert against its CAP document — some CAP fetches failed or were skipped due to the time budget. The library refuses to advance the success clock (fetchedAt stays at the previous value), writes ERROR-state diagnostics plus any still-valid retained records from the previous snapshot, rebuilds the Canada alerts union, and only then throws so the publish pipeline records the source as degraded rather than fresh.

Solutions

  1. Check the logged diagnostics line (capVerificationFailed / capSkippedDeadline / reasons) to see which failure reason dominated and target that.
  2. If skippedDeadline dominates, raise CAP_PHASE_BUDGET_MS or CAP_CONCURRENCY in scripts/lib/saskalert.mjs so more CAP docs hydrate in time.
  3. If reasons show 'timeout' or 'http', treat as upstream degradation and let the next scheduled run retry; the previous snapshot is retained meanwhile.
  4. If reasons show 'invalid_link', verify the allowlist (SASKALERT_HOST) covers the CAP link hosts the feed now emits.
  5. Rely on the retention path: unverified-but-unexpired previous alerts remain published, so downstream consumers stay fed while verification is retried.

Example fix

// before
// caller treats any throw as total failure
class SaskAlertError extends Error { constructor(msg, code) { super(msg); this.code = code; } }
// after
try {
  await publishSaskAlerts(data, { canonicalKey, ttlSeconds });
} catch (err) {
  if (err?.code === 'CAP_VERIFICATION_FAILED') {
    console.warn('SK alerts degraded; retained previous snapshot, will retry next cycle');
    return; // do not page: success clock intentionally unchanged
  }
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (data?._capVerification && (data._capVerification.failed > 0 || data._capVerification.skippedDeadline > 0)) console.warn('SK snapshot will publish as degraded: ', data._capVerification);

Try / catch

try {
  await saskAlertBeforePublish(data, { canonicalKey, ttlSeconds });
} catch (err) {
  if (err?.code === 'CAP_VERIFICATION_FAILED') {
    // expected degraded path: previous snapshot retained, success clock unchanged
    logger.warn({ key: canonicalKey, verification: data._capVerification }, 'SK alerts degraded');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Any fetchSaskAlerts result whose _capVerification shows failed > 0 or skippedDeadline > 0 passed into saskAlertBeforePublish — i.e. one or more per-alert CAP document fetches returned HTTP errors, timed out, failed parsing/validation, or were skipped because the CAP_PHASE_BUDGET_MS deadline elapsed before hydration finished.

Common situations: Saskatchewan's CAP endpoints are slow or partially down during a large weather event; too many active alerts to hydrate within the time budget; transient network failures from the worker host; CAP links in the feed pointing at hosts not on the allowlist.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/3cbd525645fc3e80. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/saskalert.mjs:394

      && (!alert.expires || Date.parse(alert.expires) > nowMs)
      && Number.isFinite(alert.updatedAt ?? alert.publishedAt)
      && nowMs - (alert.updatedAt ?? alert.publishedAt) <= SASKALERT_MAX_CONTENT_AGE_MIN * 60_000)
    : [];
  const verifiedIds = new Set(data.alerts.map(alert => alert.id));
  const snapshot = { alerts: [...data.alerts, ...retained.filter(alert => !verifiedIds.has(alert.id))] };
  const contentAge = { ...saskAlertContentMeta(snapshot, nowMs), maxContentAgeMin: SASKALERT_MAX_CONTENT_AGE_MIN };
  const metaPatch = { ...diagnostics, lastAttemptAt: nowMs, retainedRecords: retained.length };
  // An incomplete attempt can remove ended records, but cannot advance the success clock.
  await writeExtraKey(canonicalKey, snapshot, ttlSeconds, {
    ...previousMeta, fetchedAt, recordCount: snapshot.alerts.length,
    sourceVersion: 'saskalert-v1', schemaVersion: 1, state: 'ERROR',
    errorReason: 'CAP_VERIFICATION_FAILED', ...contentAge,
  });
  const written = await writeFreshnessMetadataSafely('alerts', 'saskalert', snapshot.alerts.length,
    'saskalert-v1', ttlSeconds, fetchedAt, contentAge, metaPatch);
  if (!written) throw new Error('saskalert: failed to persist verification diagnostics');
  await rebuildCanadaAlertsUnion({ currentSource: { province: 'SK', snapshot, metaPatch: { ...metaPatch, fetchedAt } } });
  throw Object.assign(new Error('saskalert: incomplete CAP verification; success clock unchanged'), { code: 'CAP_VERIFICATION_FAILED' });
}

export function saskAlertPublishTransform(data) {
  return { alerts: Array.isArray(data?.alerts) ? data.alerts : [] };
}

export function saskAlertAfterPublish(data) {
  const failed = Math.min(100, Math.max(0, Number(data?._capVerification?.failed) || 0));
  const skippedDeadline = Math.min(100, Math.max(0, Number(data?._capVerification?.skippedDeadline) || 0));
  if (failed > 0 || skippedDeadline > 0) {
    return {
      freshnessMetaPatch: {
        sourceState: 'degraded',
        errorCode: 'CAP_VERIFICATION_FAILED',
        capVerificationFailed: failed,
        capSkippedDeadline: skippedDeadline,
        capFailureReasons: data._capVerification.reasons,
      },

View on GitHub (pinned to 7d06c8633d)