koala73/worldmonitor · error · Error

GDACS malformed response

Error message

GDACS malformed response

What it means

After a successful GDACS fetch, fetchGdacs parses the body as JSON and requires it to be a GeoJSON FeatureCollection, i.e. data?.features must be an array. This error is thrown when the response is valid HTTP/JSON but not in the expected GeoJSON shape (missing 'features', wrong root type, or an HTML/error page served as JSON-ish text).

Solutions

  1. Inspect the raw body (res.text()) to see what GDACS actually returned — an HTML page means you are being blocked or proxied
  2. Confirm GDACS is serving GeoJSON: curl the GDACS_API URL with the same Accept/User-Agent headers and check the root object has type:'FeatureCollection'
  3. If a WAF/consent page is returned with 200, adjust headers (realistic User-Agent, Accept: application/json) or use an allowlisted egress IP
  4. Wrap res.json() in its own try/catch to distinguish invalid JSON from valid-but-wrong-shape JSON
  5. Add a schema check that logs JSON.stringify(data).slice(0, 200) on failure to speed diagnosis of upstream format changes

Example fix

// before
const data = await res.json();
if (!Array.isArray(data?.features)) throw new Error('GDACS malformed response');
// after
let data;
try {
  data = await res.json();
} catch (err) {
  throw new Error(`GDACS returned non-JSON body: ${err.message}`);
}
if (!Array.isArray(data?.features)) {
  throw new Error(`GDACS malformed response: ${JSON.stringify(data).slice(0, 200)}`);
}
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeGdacsFeatureCollection(data) {
  return data !== null && typeof data === 'object'
    && data.type === 'FeatureCollection'
    && Array.isArray(data.features);
}
const data = await res.json();
if (!looksLikeGdacsFeatureCollection(data)) throw new Error('GDACS malformed response');

Type guard

function isGdacsFeatureCollection(v) {
  return typeof v === 'object' && v !== null
    && v.type === 'FeatureCollection'
    && Array.isArray(v.features);
}

Try / catch

try {
  const data = await res.json();
  if (!isGdacsFeatureCollection(data)) throw new Error('GDACS malformed response');
} catch (err) {
  console.error('GDACS payload rejected:', err.message);
  return fallbackEvents(); // use cached/seeded events
}

Prevention

When it happens

Trigger: Calling fetchGdacs() when GDACS returns 200 with an unexpected body: an HTML maintenance or consent page, a JSON error object like {error: ...} with no features array, a truncated response, or a null/empty body.

Common situations: GDACS behind a captive-portal or WAF returning an HTML block page with status 200; GDACS changing its API response schema; proxy/CDN error pages served with 200; body intercepted and rewritten by a corporate proxy.

Understand the failure class

Related errors


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

Appendix: source

Thrown at scripts/seed-natural-events.mjs:219

  const pressureMatch = desc.match(/(\d{3,4})\s*(?:mb|hPa|mbar)/i);
  if (pressureMatch) {
    const p = parseInt(pressureMatch[1], 10);
    if (p >= 850 && p <= 1050) fields.pressureMb = p;
  }

  return fields;
}

async function fetchGdacs(fetchFn = globalThis.fetch) {
  const res = await fetchFn(GDACS_API, {
    headers: { Accept: 'application/json', 'User-Agent': CHROME_UA },
    signal: AbortSignal.timeout(15_000),
  });
  if (!res.ok) throw new Error(`GDACS ${res.status}`);

  const data = await res.json();
  if (!Array.isArray(data?.features)) throw new Error('GDACS malformed response');
  const features = data.features;
  const seen = new Set();
  const events = [];

  for (const f of features) {
    if (!f.geometry || f.geometry.type !== 'Point') continue;
    const props = f.properties;
    const key = `${props.eventtype}-${props.eventid}`;
    if (seen.has(key)) continue;
    seen.add(key);

    if (props.alertlevel === 'Green') continue;

    const category = GDACS_TO_CATEGORY[props.eventtype] || 'manmade';
    const alertPrefix = props.alertlevel === 'Red' ? '\u{1F534} ' : props.alertlevel === 'Orange' ? '\u{1F7E0} ' : '';
    const description = props.description || EVENT_TYPE_NAMES[props.eventtype] || props.eventtype;
    const severity = props.severitydata?.severitytext || '';

View on GitHub (pinned to 7d06c8633d)