koala73/worldmonitor · error · NhcQueryError
NHC layer ${layerId}: ${cause.message}
Error message
NHC layer ${layerId}: ${cause.message} What it means
nhcQuery fetches an NHC ArcGIS GeoJSON layer with a 15-second timeout. When the response status is not ok it cancels the body and wraps httpRetryError(res) — which derives a message and retryability from the HTTP status — into NhcQueryError with message `NHC layer <layerId>: <cause.message>`. nonRetryable and retryAfterMs are inherited from the cause, so 429/5xx may be retried by withRetry while 4xx failures are terminal.
Solutions
- Read the wrapped cause.message (the httpRetryError result) to get the exact HTTP status, then act on it specifically.
- For 429, honor retryAfterMs (propagated onto the error) and add backoff/staggering between layer queries.
- For 404, resolve the current layer ID from the ArcGIS services directory and update NHC_STORM_SLOTS offsets.
- Retry later for 5xx/503 — the NHC service is often transiently unavailable; the withRetry wrapper may already cover retryable statuses.
- Verify network/proxy access to NHC_BASE from the machine running the seed.
Example fix
// before
const res = await fetchFn(url, { headers, signal });
// after: log status and back off on 429 before retrying the whole seed
const res = await fetchFn(url, { headers, signal });
if (res.status === 429) {
const retryAfter = Number(res.headers.get('retry-after')) || 30;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
} Defensive patterns
Strategy: retry
Validate before calling
const url = `${NHC_BASE}/${layerId}/query?where=1%3D1&outFields=*&f=geojson`;
const head = await fetch(url, { method: 'HEAD' });
if (!head.ok) throw new Error(`layer ${layerId} unavailable: HTTP ${head.status}`); // fail before the full seed run
await head.body?.cancel?.(); Type guard
function isRetryableNhcFailure(err) {
return err instanceof NhcQueryError && !err.nonRetryable;
} Try / catch
try {
const layer = await nhcQuery(layerId, expectedGeometryTypes);
return layer;
} catch (err) {
if (err instanceof NhcQueryError && err.retryAfterMs) {
await sleep(err.retryAfterMs);
return nhcQuery(layerId, expectedGeometryTypes); // single manual retry for 429
}
if (err instanceof NhcQueryError && err.nonRetryable) {
logger.error({ layerId, cause: err.cause?.message }, 'NHC layer permanently unavailable');
return null;
}
throw err;
} Prevention
- Stagger requests between NHC layers to stay under ArcGIS rate limits.
- Resolve layer IDs from the ArcGIS services directory instead of hardcoding offsets that 404 after seasonal rotation.
- Honor Retry-After / retryAfterMs on 429 responses before re-running.
- Check https://www.nhc.noaa.gov/gis/ or the service status page when 5xx/503 errors repeat.
When it happens
Trigger: The NHC layer query URL returns a non-2xx status: 404 after an NHC layer renumbering, 400 from a bad layerId, 429 rate limiting from repeated seed runs, 503 during NHC/ArcGIS maintenance, or any 5xx outage.
Common situations: Burst-running the seeder trips ArcGIS rate limits; NHC rotates service layer IDs between hurricane seasons so hardcoded slot indexes 404; corporate proxy blocks the request and returns an error page; NHC takes the GIS service offline during off-season maintenance.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Cloudflare Radar API error: ${resp.status}
- Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${d
- Redis HTTP ${resp.status}
- HTTP ${response.status}
- HTTP ${response.status}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/fba2bbc8dd14d3bb.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seed-natural-events.mjs:351
code: 'NHC_POINT_RESPONSE_INVALID',
nonRetryable: true,
});
}
}
return payload;
}
async function nhcQuery(layerId, expectedGeometryTypes, fetchFn = globalThis.fetch) {
const url = `${NHC_BASE}/${layerId}/query?where=1%3D1&outFields=*&f=geojson`;
return withRetry(async () => {
const res = await fetchFn(url, {
headers: { Accept: 'application/json', 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
await res.body?.cancel?.();
const cause = httpRetryError(res, { remainingBudgetMs: 15_000 });
throw new NhcQueryError(`NHC layer ${layerId}: ${cause.message}`, {
cause,
nonRetryable: cause.nonRetryable,
});
}
let payload;
try {
payload = await res.json();
} catch (cause) {
const bodyTransportFailure = cause instanceof TypeError
|| cause?.name === 'AbortError'
|| cause?.name === 'TimeoutError';
throw new NhcQueryError(
`NHC layer ${layerId} ${bodyTransportFailure ? 'body read failed' : 'returned invalid JSON'}`,
{
code: bodyTransportFailure ? 'NHC_POINT_REQUEST_FAILED' : 'NHC_POINT_RESPONSE_INVALID',
cause,
nonRetryable: !bodyTransportFailure,
},View on GitHub (pinned to 7d06c8633d)