koala73/worldmonitor · error · NhcQueryError
NHC_POINT_RESPONSE_INVALID
NHC_POINT_RESPONSE_INVALID
Error message
NHC layer ${layerId} did not return a FeatureCollection What it means
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.
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
Example fix
// before
const payload = await res.json();
return parseNhcGeoJson(payload, layerId, EXPECTED);
// after
const payload = await res.json();
if (payload?.error) {
throw new NhcQueryError(`NHC layer ${layerId} service error: ${payload.error.message}`, { code: 'NHC_POINT_RESPONSE_INVALID', nonRetryable: true });
}
return parseNhcGeoJson(payload, layerId, EXPECTED); Defensive patterns
Strategy: validation
Validate before calling
function isCompleteNhcFeatureCollection(payload) {
return payload !== null && typeof payload === 'object'
&& payload.type === 'FeatureCollection'
&& Array.isArray(payload.features)
&& payload.exceededTransferLimit !== true;
}
const payload = await res.json();
if (!isCompleteNhcFeatureCollection(payload)) throw new NhcQueryError('incomplete FeatureCollection', { code: 'NHC_POINT_RESPONSE_INVALID', nonRetryable: true }); Type guard
function isNhcFeatureCollection(v) {
return typeof v === 'object' && v !== null
&& v.type === 'FeatureCollection'
&& Array.isArray(v.features)
&& v.exceededTransferLimit !== true;
} Try / catch
try {
return parseNhcGeoJson(payload, layerId, expectedTypes);
} catch (err) {
if (err.code === 'NHC_POINT_RESPONSE_INVALID' && err.nonRetryable) {
console.error(`Skipping NHC layer ${layerId}: ${err.message}`);
return null; // skip layer, continue seeding others
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- GDACS malformed response
- Physical divergence snapshot must contain gold and silver re
- ECCC_MALFORMED_PAGE
- ${name} cronSchedule must be a string or null
- Compact health payload must be an object
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/eabfbea13492b549.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seed-natural-events.mjs:306
this.nonRetryable = nonRetryable;
if (Number.isFinite(cause?.retryAfterMs)) this.retryAfterMs = cause.retryAfterMs;
}
}
function validCoordinates(value, depth = 0) {
if (!Array.isArray(value) || value.length === 0 || depth > 4) return false;
if (value.every(Number.isFinite)) {
return value.length >= 2
&& Math.abs(value[0]) <= 180
&& Math.abs(value[1]) <= 90;
}
return value.every((entry) => validCoordinates(entry, depth + 1));
}
function parseNhcGeoJson(payload, layerId, expectedGeometryTypes) {
if (!payload || typeof payload !== 'object' || payload.type !== 'FeatureCollection'
|| !Array.isArray(payload.features) || payload.exceededTransferLimit === true) {
throw new NhcQueryError(`NHC layer ${layerId} did not return a FeatureCollection`, {
code: 'NHC_POINT_RESPONSE_INVALID',
nonRetryable: true,
});
}
for (const feature of payload.features) {
const geometry = feature?.geometry;
if (!feature || typeof feature !== 'object' || !geometry
|| !expectedGeometryTypes.has(geometry.type) || !validCoordinates(geometry.coordinates)
|| !feature.properties || typeof feature.properties !== 'object' || Array.isArray(feature.properties)) {
throw new NhcQueryError(`NHC layer ${layerId} returned a malformed feature`, {
code: 'NHC_POINT_RESPONSE_INVALID',
nonRetryable: true,
});
}
}
return payload;
}
View on GitHub (pinned to 7d06c8633d)