{"record":{"id":"81112609429044b2","repo":"koala73/worldmonitor","slug":"eonet-malformed-response","errorCode":null,"errorMessage":"EONET malformed response","messagePattern":"EONET malformed response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/seed-natural-events.mjs","lineNumber":98,"sourceCode":"  'landslides', 'drought', 'dustHaze', 'snow', 'tempExtremes',\n  'seaLakeIce', 'waterColor', 'manmade',\n]);\n\nfunction normalizeCategory(id) {\n  const c = String(id || '').trim();\n  return NATURAL_EVENT_CATEGORIES.has(c) ? c : 'manmade';\n}\n\nasync function fetchEonet(days, fetchFn = globalThis.fetch) {\n  const url = `${EONET_API_URL}?status=open&days=${days}`;\n  const res = await fetchFn(url, {\n    headers: { Accept: 'application/json', 'User-Agent': CHROME_UA },\n    signal: AbortSignal.timeout(15_000),\n  });\n  if (!res.ok) throw new Error(`EONET ${res.status}`);\n\n  const data = await res.json();\n  if (!Array.isArray(data?.events)) throw new Error('EONET malformed response');\n  const events = [];\n  const now = Date.now();\n\n  for (const event of data.events || []) {\n    const category = event.categories?.[0];\n    if (!category) continue;\n    const normalizedCategory = normalizeCategory(category.id);\n    if (normalizedCategory === 'earthquakes') continue;\n\n    const latestGeo = event.geometry?.[event.geometry.length - 1];\n    if (!latestGeo || latestGeo.type !== 'Point') continue;\n\n    const eventDate = new Date(latestGeo.date);\n    const [lon, lat] = latestGeo.coordinates;\n\n    if (normalizedCategory === 'wildfires' && now - eventDate.getTime() > WILDFIRE_MAX_AGE_MS) continue;\n\n    const source = event.sources?.[0];","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/seed-natural-events.mjs#L80-L116","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst data = await res.json();\nif (!Array.isArray(data?.events)) throw new Error('EONET malformed response');\n// after\nconst text = await res.text();\nlet data;\ntry { data = JSON.parse(text); } catch { /* fallthrough */ }\nif (!Array.isArray(data?.events)) {\n  throw new Error(`EONET malformed response: ${text.slice(0, 200)}`);\n}","handlingStrategy":"validation","validationCode":"// Check the body shape before consuming events:\nconst data = await res.json();\nconst eventsIsArray = data != null && typeof data === 'object' && Array.isArray(data.events);\nif (!eventsIsArray) console.warn('EONET 200 response does not contain an events array — check proxy/endpoint');","typeGuard":"function isEonetPayload(data) {\n  return data != null && typeof data === 'object' && Array.isArray(data.events);\n}","tryCatchPattern":"try {\n  const events = await fetchEonet(days);\n} catch (err) {\n  if (err.message === 'EONET malformed response') {\n    // 200 but wrong shape: log raw body snippet, treat as degraded rather than crash\n    logDegraded('eonet', 'unexpected 200 payload');\n  } else throw err;\n}","preventionTips":["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."],"tags":["schema","eonet","nasa","seed-script","response-validation"],"backgroundTag":"unexpected-response-shape","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}