{"record":{"id":"fba2bbc8dd14d3bb","repo":"koala73/worldmonitor","slug":"nhc-layer-layerid-cause-message","errorCode":null,"errorMessage":"NHC layer ${layerId}: ${cause.message}","messagePattern":"NHC layer (.+?): (.+?)","errorType":"http","errorClass":"NhcQueryError","httpStatus":null,"severity":"error","filePath":"scripts/seed-natural-events.mjs","lineNumber":351,"sourceCode":"        code: 'NHC_POINT_RESPONSE_INVALID',\n        nonRetryable: true,\n      });\n    }\n  }\n  return payload;\n}\n\nasync function nhcQuery(layerId, expectedGeometryTypes, fetchFn = globalThis.fetch) {\n  const url = `${NHC_BASE}/${layerId}/query?where=1%3D1&outFields=*&f=geojson`;\n  return withRetry(async () => {\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) {\n      await res.body?.cancel?.();\n      const cause = httpRetryError(res, { remainingBudgetMs: 15_000 });\n      throw new NhcQueryError(`NHC layer ${layerId}: ${cause.message}`, {\n        cause,\n        nonRetryable: cause.nonRetryable,\n      });\n    }\n    let payload;\n    try {\n      payload = await res.json();\n    } catch (cause) {\n      const bodyTransportFailure = cause instanceof TypeError\n        || cause?.name === 'AbortError'\n        || cause?.name === 'TimeoutError';\n      throw new NhcQueryError(\n        `NHC layer ${layerId} ${bodyTransportFailure ? 'body read failed' : 'returned invalid JSON'}`,\n        {\n          code: bodyTransportFailure ? 'NHC_POINT_REQUEST_FAILED' : 'NHC_POINT_RESPONSE_INVALID',\n          cause,\n          nonRetryable: !bodyTransportFailure,\n        },","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/seed-natural-events.mjs#L333-L369","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst res = await fetchFn(url, { headers, signal });\n// after: log status and back off on 429 before retrying the whole seed\nconst res = await fetchFn(url, { headers, signal });\nif (res.status === 429) {\n  const retryAfter = Number(res.headers.get('retry-after')) || 30;\n  await new Promise((r) => setTimeout(r, retryAfter * 1000));\n}","handlingStrategy":"retry","validationCode":"const url = `${NHC_BASE}/${layerId}/query?where=1%3D1&outFields=*&f=geojson`;\nconst head = await fetch(url, { method: 'HEAD' });\nif (!head.ok) throw new Error(`layer ${layerId} unavailable: HTTP ${head.status}`); // fail before the full seed run\nawait head.body?.cancel?.();","typeGuard":"function isRetryableNhcFailure(err) {\n  return err instanceof NhcQueryError && !err.nonRetryable;\n}","tryCatchPattern":"try {\n  const layer = await nhcQuery(layerId, expectedGeometryTypes);\n  return layer;\n} catch (err) {\n  if (err instanceof NhcQueryError && err.retryAfterMs) {\n    await sleep(err.retryAfterMs);\n    return nhcQuery(layerId, expectedGeometryTypes); // single manual retry for 429\n  }\n  if (err instanceof NhcQueryError && err.nonRetryable) {\n    logger.error({ layerId, cause: err.cause?.message }, 'NHC layer permanently unavailable');\n    return null;\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["http","network","api-error-response","rate-limit"],"backgroundTag":"http-error-response","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"}