koala73/worldmonitor · error · Error

ECCC_MALFORMED_PAGE

ECCC_MALFORMED_PAGE

Error message

ECCC_MALFORMED_PAGE

What it means

After each page fetch, the ECCC paging loop validates the OGC API Features response envelope: data.type must be 'FeatureCollection', numberMatched and numberReturned must be non-negative safe integers, numberReturned must equal the validated feature count from requireAlertFeatures(), and the page length must not exceed ECCC_PAGE_SIZE (250). Any violation throws Error('ECCC_MALFORMED_PAGE') because paging arithmetic (offset, loop termination) depends on these fields being truthful.

Solutions

  1. Log the offending page (or capture failureDetail from the aggregate error) and compare its shape against the OGC API Features contract: { type:'FeatureCollection', numberMatched, numberReturned, features }.
  2. Check https://api.weather.gc.ca/collections/weather-alerts/items?f=json&limit=1 directly for an upstream schema change; update the validator if the collection legitimately moved.
  3. If a test stub caused it, make the mock return a full FeatureCollection envelope with consistent numberMatched/numberReturned and at most ECCC_PAGE_SIZE features.
  4. Verify requireAlertFeatures() (the earlier validation step) and this envelope check are not duplicating/contradicting constraints after a refactor.

Example fix

// before: stub returns bare array
const data = { features: [...250 features] };
// after: full OGC envelope matching the contract
const data = { type: 'FeatureCollection', numberMatched: 250, numberReturned: 250, features: [...250 features] };
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeOgcFeatureCollection(data) {
  return data?.type === 'FeatureCollection'
    && Array.isArray(data.features)
    && Number.isSafeInteger(data.numberMatched) && data.numberMatched >= 0
    && Number.isSafeInteger(data.numberReturned) && data.numberReturned === data.features.length
    && data.features.length <= 250;
}

Type guard

function isEcccAlertPage(data) {
  return typeof data === 'object' && data !== null
    && data.type === 'FeatureCollection'
    && Number.isSafeInteger(data.numberMatched)
    && Number.isSafeInteger(data.numberReturned)
    && Array.isArray(data.features);
}

Try / catch

try {
  const result = await fetchEcccAlertFeatures({ fetchFn, userAgent });
} catch (err) {
  if (String(err.message).includes('ECCC_MALFORMED_PAGE')) {
    reportHealth('eccc', { ok: false, reason: 'malformed-page', detail: err.message });
  } else throw err;
}

Prevention

When it happens

Trigger: The response from https://api.weather.gc.ca/collections/weather-alerts/items in scripts/_weather-alert-select.mjs:775 is a valid JSON object passing requireAlertFeatures() but violates the envelope contract: wrong type field, missing/non-integer/negative numberMatched or numberReturned, numberReturned mismatching the actual features array length, or more than 250 features in one page.

Common situations: The ECCC API changes its GeoJSON envelope shape or migrates to a different OGC API version; a proxy or CDN strips/alters fields; a test stub returns a plain features array without the FeatureCollection metadata; the server returns an error page that still parses as JSON; limit parameter silently ignored so a page exceeds 250 features.

Understand the failure class

Related errors


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

Appendix: source

Thrown at scripts/_weather-alert-select.mjs:775

  for (const [index, status] of ECCC_LIVE_STATUSES.entries()) {
    const statusFeatures = [];
    let matched;
    try {
      do {
        if (pages >= ECCC_MAX_PAGES) throw new Error('ECCC_PAGE_LIMIT');
        if (byteBudget.remaining <= 0) throw new Error('ECCC_AGGREGATE_TOO_LARGE');
        const url = new URL(ECCC_ALERTS_URLS[index]);
        url.searchParams.set('offset', String(statusFeatures.length));
        pages += 1;
        const data = await fetchApprovedWeatherJson(url.toString(), {
          allowedHosts: [ECCC_HOST], maxBytes, fetchFn, userAgent, byteBudget,
        });
        const page = requireAlertFeatures(data);
        if (data.type !== 'FeatureCollection'
          || !Number.isSafeInteger(data.numberMatched) || data.numberMatched < 0
          || !Number.isSafeInteger(data.numberReturned) || data.numberReturned !== page.length
          || page.length > ECCC_PAGE_SIZE) {
          throw new Error('ECCC_MALFORMED_PAGE');
        }
        if (matched !== undefined && data.numberMatched !== matched) throw new Error('ECCC_COUNT_DRIFT');
        matched = data.numberMatched;
        if (statusFeatures.length + page.length > matched
          || (page.length === 0 && statusFeatures.length < matched)) {
          throw new Error('ECCC_PAGE_PROGRESS');
        }
        for (const feature of page) {
          if (typeof feature?.id !== 'string' || !feature.id.trim()) throw new Error('ECCC_INVALID_ID');
          if (seenIds.has(feature.id)) throw new Error('ECCC_DUPLICATE_ID');
          seenIds.add(feature.id);
        }
        statusFeatures.push(...page);
      } while (statusFeatures.length < matched);
      features.push(...statusFeatures);
    } catch (err) {
      failures.push(err);
      failedStatuses.push(status);

View on GitHub (pinned to 7d06c8633d)