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
- 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 }.
- 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.
- 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.
- 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
- In test stubs, always return the full OGC envelope (type, numberMatched, numberReturned, features) with consistent counts.
- Pin/monitor the ECCC API schema; envelope changes surface as this error first.
- Run a smoke fetch of one page in CI to catch upstream shape drift before production.
- Validate a single page with looksLikeOgcFeatureCollection before running the multi-page fetch in tooling.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ECCC_PAGE_PROGRESS
- relay returned ${resp.status}
- Exa search failed HTTP ${resp.status}: ${text.slice(0, 120)}
- Wikidata request failed with status ${response.status}
- Wikidata request failed
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)