{"record":{"id":"eabfbea13492b549","repo":"koala73/worldmonitor","slug":"nhc-point-response-invalid","errorCode":"NHC_POINT_RESPONSE_INVALID","errorMessage":"NHC layer ${layerId} did not return a FeatureCollection","messagePattern":"NHC layer (.+?) did not return a FeatureCollection","errorType":"exception","errorClass":"NhcQueryError","httpStatus":null,"severity":"error","filePath":"scripts/seed-natural-events.mjs","lineNumber":306,"sourceCode":"    this.nonRetryable = nonRetryable;\n    if (Number.isFinite(cause?.retryAfterMs)) this.retryAfterMs = cause.retryAfterMs;\n  }\n}\n\nfunction validCoordinates(value, depth = 0) {\n  if (!Array.isArray(value) || value.length === 0 || depth > 4) return false;\n  if (value.every(Number.isFinite)) {\n    return value.length >= 2\n      && Math.abs(value[0]) <= 180\n      && Math.abs(value[1]) <= 90;\n  }\n  return value.every((entry) => validCoordinates(entry, depth + 1));\n}\n\nfunction parseNhcGeoJson(payload, layerId, expectedGeometryTypes) {\n  if (!payload || typeof payload !== 'object' || payload.type !== 'FeatureCollection'\n    || !Array.isArray(payload.features) || payload.exceededTransferLimit === true) {\n    throw new NhcQueryError(`NHC layer ${layerId} did not return a FeatureCollection`, {\n      code: 'NHC_POINT_RESPONSE_INVALID',\n      nonRetryable: true,\n    });\n  }\n  for (const feature of payload.features) {\n    const geometry = feature?.geometry;\n    if (!feature || typeof feature !== 'object' || !geometry\n      || !expectedGeometryTypes.has(geometry.type) || !validCoordinates(geometry.coordinates)\n      || !feature.properties || typeof feature.properties !== 'object' || Array.isArray(feature.properties)) {\n      throw new NhcQueryError(`NHC layer ${layerId} returned a malformed feature`, {\n        code: 'NHC_POINT_RESPONSE_INVALID',\n        nonRetryable: true,\n      });\n    }\n  }\n  return payload;\n}\n","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/seed-natural-events.mjs#L288-L324","documentation":"parseNhcGeoJson validates that each NOAA NHC layer response is a complete GeoJSON FeatureCollection. This error is thrown when the payload is not an object, its type is not 'FeatureCollection', features is not an array, or exceededTransferLimit is true — meaning the ArcGIS query endpoint paged the result and the seeder would silently miss storms. It carries code NHC_POINT_RESPONSE_INVALID and nonRetryable:true because re-fetching the same layer URL will not help.","triggerScenarios":"Calling parseNhcGeoJson(payload, layerId, expectedGeometryTypes) with: an error JSON body from the NHC ArcGIS service, a single Feature instead of a FeatureCollection, a truncated/partial response, or a layer whose result set exceeds the ArcGIS maxRecordCount so exceededTransferLimit:true is set.","commonSituations":"NHC service returning {error:{code:...}} JSON with HTTP 200 during outages; querying a layers/…/query endpoint without enough where/outFields params so pagination kicks in during active hurricane season with many features; mistyped layer URL returning an HTML error page.","solutions":["Since nonRetryable:true, do not blind-retry; inspect the payload — log JSON.stringify(payload).slice(0,200) at the fetch site to see the actual body","If exceededTransferLimit was true, raise the query's resultRecordCount/resultOffset pagination or request fewer fields so all features fit under maxRecordCount","Verify the layer query URL returns {type:'FeatureCollection', features:[...]} — add f=geojson to the ArcGIS query parameters if missing","Handle NHC service {error:...} bodies by checking HTTP status and payload.error before parsing, and surface the NHC service error message","Because the code is marked non-retryable, make the seeder skip the layer and continue seeding other datasets instead of crashing the whole run"],"exampleFix":"// before\nconst payload = await res.json();\nreturn parseNhcGeoJson(payload, layerId, EXPECTED);\n// after\nconst payload = await res.json();\nif (payload?.error) {\n  throw new NhcQueryError(`NHC layer ${layerId} service error: ${payload.error.message}`, { code: 'NHC_POINT_RESPONSE_INVALID', nonRetryable: true });\n}\nreturn parseNhcGeoJson(payload, layerId, EXPECTED);","handlingStrategy":"validation","validationCode":"function isCompleteNhcFeatureCollection(payload) {\n  return payload !== null && typeof payload === 'object'\n    && payload.type === 'FeatureCollection'\n    && Array.isArray(payload.features)\n    && payload.exceededTransferLimit !== true;\n}\nconst payload = await res.json();\nif (!isCompleteNhcFeatureCollection(payload)) throw new NhcQueryError('incomplete FeatureCollection', { code: 'NHC_POINT_RESPONSE_INVALID', nonRetryable: true });","typeGuard":"function isNhcFeatureCollection(v) {\n  return typeof v === 'object' && v !== null\n    && v.type === 'FeatureCollection'\n    && Array.isArray(v.features)\n    && v.exceededTransferLimit !== true;\n}","tryCatchPattern":"try {\n  return parseNhcGeoJson(payload, layerId, expectedTypes);\n} catch (err) {\n  if (err.code === 'NHC_POINT_RESPONSE_INVALID' && err.nonRetryable) {\n    console.error(`Skipping NHC layer ${layerId}: ${err.message}`);\n    return null; // skip layer, continue seeding others\n  }\n  throw err;\n}","preventionTips":["Always request f=geojson from ArcGIS/NHC query endpoints","Paginate with resultOffset/resultRecordCount so exceededTransferLimit never triggers","Check payload.error before parsing since NHC can return error JSON with HTTP 200","Treat this code as non-retryable and skip the layer rather than crash the run"],"tags":["geojson","nhc","schema","seeding"],"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"}