koala73/worldmonitor · error
EONET malformed response
Error message
EONET malformed response
What it means
After a successful (2xx) EONET response, fetchEonet parses JSON and validates the envelope: `if (!Array.isArray(data?.events)) throw new Error('EONET malformed response')`. This fires when EONET answers 200 but the body is not the expected `{ events: [...] }` shape — typically an HTML login/error page served with 200, an empty/HTML proxy response, or an EONET schema change. It protects the seed loop (which iterates data.events and reads event.categories) from undefined-property crashes.
Solutions
- Log the first ~200 chars of the raw body on this error to see whether it is HTML (proxy/portal) or JSON with a different schema.
- Verify EONET_API_URL points at the current EONET v3 endpoint (`https://eonet.gsfc.nasa.gov/api/v3/events`) and that the env/config value is not overridden.
- If HTML with status 200, fix the network path (proxy exceptions, no captive portal) rather than the code.
- Keep the guard: it is correct — optionally widen it to also reject an empty non-object body and include a body snippet in the message for diagnosability.
Example fix
// before
const data = await res.json();
if (!Array.isArray(data?.events)) throw new Error('EONET malformed response');
// after
const text = await res.text();
let data;
try { data = JSON.parse(text); } catch { /* fallthrough */ }
if (!Array.isArray(data?.events)) {
throw new Error(`EONET malformed response: ${text.slice(0, 200)}`);
} Defensive patterns
Strategy: validation
Validate before calling
// Check the body shape before consuming events:
const data = await res.json();
const eventsIsArray = data != null && typeof data === 'object' && Array.isArray(data.events);
if (!eventsIsArray) console.warn('EONET 200 response does not contain an events array — check proxy/endpoint'); Type guard
function isEonetPayload(data) {
return data != null && typeof data === 'object' && Array.isArray(data.events);
} Try / catch
try {
const events = await fetchEonet(days);
} catch (err) {
if (err.message === 'EONET malformed response') {
// 200 but wrong shape: log raw body snippet, treat as degraded rather than crash
logDegraded('eonet', 'unexpected 200 payload');
} else throw err;
} Prevention
- Pin and periodically verify EONET_API_URL against the current v3 API; a stale URL can return an HTML page.
- Log a snippet of the raw body on validation failure to distinguish proxy HTML from schema drift.
- Beware captive portals/proxies in CI networks that answer 200 with HTML.
- Subscribe to EONET/NASA service notices for envelope changes before they break the seed.
When it happens
Trigger: EONET returns HTTP 200 whose JSON body lacks an `events` array: captive-portal/proxy HTML interstitial (JSON.parse may even throw first), API version drift renaming/moving `events`, or a `data` object where events is null/string instead of an array.
Common situations: Corporate proxy or Wi-Fi captive portal injecting a 200 HTML page; hitting an EONET endpoint URL that changed (stale EONET_API_URL env/config); NASA altering the response envelope in a new API version.
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
- EONET
- CF Radar DDoS API error: protocol=
- CF Radar traffic anomalies API error
- Compact health payload must be an object
- Compact health pending entries must be objects
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/81112609429044b2.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seed-natural-events.mjs:248
details = ' details={"unavailable":true}';
}
}
const error = Object.assign(new Error(`${source} ${stage} ${kind} attempt=${attempt} elapsedMs=${Math.round(finished - started)} attemptElapsedMs=${Math.round(finished - attemptStarted)}${phaseTimings}${details}`), {
nonRetryable: stage === 'http' ? cause.nonRetryable : !transport,
retryAfterMs: cause.retryAfterMs,
});
if (deadline - performance.now() <= Math.max(500, error.retryAfterMs || 0)) error.nonRetryable = true;
throw error;
} finally {
await dispatcher?.destroy();
}
}, 1, 500);
}
async function fetchEonet(days, fetchFn = globalThis.fetch, now = Date.now()) {
const url = `${EONET_API_URL}?status=open&days=${days}`;
const data = await fetchEventSourceJson('eonet', url, fetchFn);
if (!Array.isArray(data?.events)) throw new Error('EONET malformed response');
const events = [];
for (const event of data.events || []) {
const category = event.categories?.[0];
if (!category) throw new Error('EONET malformed event category');
const normalizedCategory = normalizeCategory(category.id);
if (normalizedCategory === 'earthquakes') continue;
const latestGeo = event.geometry?.[event.geometry.length - 1];
if (!latestGeo || !['Point', 'Polygon'].includes(latestGeo.type) || !Array.isArray(latestGeo.coordinates)) {
throw new Error('EONET malformed event geometry');
}
if (latestGeo.type !== 'Point') continue;
const eventDate = new Date(latestGeo.date);
const [lon, lat] = latestGeo.coordinates;
if (normalizedCategory === 'wildfires' && now - eventDate.getTime() > WILDFIRE_MAX_AGE_MS) continue;View on GitHub (pinned to e586b8b4b8)