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

  1. 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
  2. If exceededTransferLimit was true, raise the query's resultRecordCount/resultOffset pagination or request fewer fields so all features fit under maxRecordCount
  3. Verify the layer query URL returns {type:'FeatureCollection', features:[...]} — add f=geojson to the ArcGIS query parameters if missing
  4. Handle NHC service {error:...} bodies by checking HTTP status and payload.error before parsing, and surface the NHC service error message
  5. 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

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


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)