{"record":{"id":"e10d33959fe2e39a","repo":"koala73/worldmonitor","slug":"bodytransportfailure-nhc-point-request-failed-nhc-point","errorCode":"bodyTransportFailure ? 'NHC_POINT_REQUEST_FAILED' : 'NHC_POINT_RESPONSE_INVALID'","errorMessage":"NHC layer ${layerId} ${bodyTransportFailure ? 'body read failed' : 'returned invalid JSON'}","messagePattern":"NHC layer (.+?) (.+?)","errorType":"exception","errorClass":"NhcQueryError","httpStatus":null,"severity":"error","filePath":"scripts/seed-natural-events.mjs","lineNumber":363,"sourceCode":"      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        },\n      );\n    }\n    return parseNhcGeoJson(payload, layerId, expectedGeometryTypes);\n  }, 1, 500);\n}\n\nconst NHC_STORM_TYPES = {\n  HU: 'Hurricane', TS: 'Tropical Storm', TD: 'Tropical Depression',\n  STS: 'Subtropical Storm', STD: 'Subtropical Depression',\n  EX: 'Post-Tropical', PT: 'Post-Tropical',\n};\n","sourceCodeStart":345,"sourceCodeEnd":381,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/seed-natural-events.mjs#L345-L381","documentation":"After a successful HTTP status, nhcQuery calls res.json(); if that throws, it classifies the failure: TypeError, AbortError, or TimeoutError mean the body stream broke mid-read (body read failed -> NHC_POINT_REQUEST_FAILED, retryable), while any other parse error means the body was complete but not valid JSON (returned invalid JSON -> NHC_POINT_RESPONSE_INVALID, nonRetryable). This distinction lets withRetry retry truncated transfers but not deterministic bad payloads.","triggerScenarios":"The NHC layer query returns a 200 whose body is HTML (an error/interstitial page), an empty body, truncated gzip/deflate content, or a connection reset mid-stream; or an AbortSignal.timeout(15_000) fires while the body is still streaming, surfacing as AbortError/TimeoutError inside res.json().","commonSituations":"A proxy/CDN serves an HTML block page with status 200; slow NHC responses exceed the 15 s timeout on large FeatureCollections; the edge closes the connection early on big layer downloads; content-encoding mismatch makes the body undecodable.","solutions":["Capture the raw body text (res.text() then JSON.parse) on failure to see whether it is HTML, empty, or truncated JSON.","For TimeoutError/AbortError, increase the timeout beyond 15 s or reduce the requested payload (filter outFields, add resultRecordCount).","For invalid JSON, check for proxy/interception returning HTML with 200 and bypass or allowlist the NHC host.","For transport failures, rely on the retryable NHC_POINT_REQUEST_FAILED path and re-run; add resume/backoff for flaky links.","Verify content-encoding handling — if the environment cannot decompress gzip, send Accept-Encoding: identity."],"exampleFix":"// before\npayload = await res.json();\n// after: capture raw text to diagnose non-JSON bodies\nconst text = await res.text();\ntry {\n  payload = JSON.parse(text);\n} catch (cause) {\n  if (text.trimStart().startsWith('<')) {\n    throw new NhcQueryError(`NHC layer ${layerId} returned HTML instead of JSON`, { code: 'NHC_POINT_RESPONSE_INVALID', cause, nonRetryable: true });\n  }\n  throw cause;\n}","handlingStrategy":"try-catch","validationCode":"const text = await res.text();\nif (!text.trim()) throw new Error('empty body');\nif (text.trimStart()[0] !== '{' && text.trimStart()[0] !== '[') throw new Error('non-JSON body (HTML/interstitial page)');\nconst payload = JSON.parse(text); // throws SyntaxError with position info for truncated JSON","typeGuard":"function isTransportBodyFailure(cause) {\n  return cause instanceof TypeError || cause?.name === 'AbortError' || cause?.name === 'TimeoutError';\n}","tryCatchPattern":"try {\n  payload = await res.json();\n} catch (cause) {\n  if (cause?.name === 'TimeoutError' || cause?.name === 'AbortError' || cause instanceof TypeError) {\n    throw new NhcQueryError(`NHC layer ${layerId} body read failed`, { code: 'NHC_POINT_REQUEST_FAILED', cause, nonRetryable: false });\n  }\n  const text = await res.clone?.().text?.().catch(() => '') ?? '';\n  logger.error({ layerId, preview: text.slice(0, 200) }, 'NHC returned non-JSON body');\n  throw new NhcQueryError(`NHC layer ${layerId} returned invalid JSON`, { code: 'NHC_POINT_RESPONSE_INVALID', cause, nonRetryable: true });\n}","preventionTips":["Keep timeouts generous for large FeatureCollections or narrow the query (outFields, resultRecordCount) to shrink the body.","Log a preview of the raw text when JSON parsing fails so HTML interception is immediately visible.","Send Accept-Encoding: identity if the runtime mishandles compressed NHC responses.","Retry transport failures (NHC_POINT_REQUEST_FAILED) with backoff; never retry NHC_POINT_RESPONSE_INVALID without changing something."],"tags":["json","network","timeout","api-response"],"backgroundTag":"invalid-json-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"}