koala73/worldmonitor · error · NhcQueryError
bodyTransportFailure ? 'NHC_POINT_REQUEST_FAILED' : 'NHC_POINT_RESPONSE_INVALID'
bodyTransportFailure ? 'NHC_POINT_REQUEST_FAILED' : 'NHC_POINT_RESPONSE_INVALID'
Error message
NHC layer ${layerId} ${bodyTransportFailure ? 'body read failed' : 'returned invalid JSON'} What it means
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.
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.
Example fix
// before
payload = await res.json();
// after: capture raw text to diagnose non-JSON bodies
const text = await res.text();
try {
payload = JSON.parse(text);
} catch (cause) {
if (text.trimStart().startsWith('<')) {
throw new NhcQueryError(`NHC layer ${layerId} returned HTML instead of JSON`, { code: 'NHC_POINT_RESPONSE_INVALID', cause, nonRetryable: true });
}
throw cause;
} Defensive patterns
Strategy: try-catch
Validate before calling
const text = await res.text();
if (!text.trim()) throw new Error('empty body');
if (text.trimStart()[0] !== '{' && text.trimStart()[0] !== '[') throw new Error('non-JSON body (HTML/interstitial page)');
const payload = JSON.parse(text); // throws SyntaxError with position info for truncated JSON Type guard
function isTransportBodyFailure(cause) {
return cause instanceof TypeError || cause?.name === 'AbortError' || cause?.name === 'TimeoutError';
} Try / catch
try {
payload = await res.json();
} catch (cause) {
if (cause?.name === 'TimeoutError' || cause?.name === 'AbortError' || cause instanceof TypeError) {
throw new NhcQueryError(`NHC layer ${layerId} body read failed`, { code: 'NHC_POINT_REQUEST_FAILED', cause, nonRetryable: false });
}
const text = await res.clone?.().text?.().catch(() => '') ?? '';
logger.error({ layerId, preview: text.slice(0, 200) }, 'NHC returned non-JSON body');
throw new NhcQueryError(`NHC layer ${layerId} returned invalid JSON`, { code: 'NHC_POINT_RESPONSE_INVALID', cause, nonRetryable: true });
} Prevention
- 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.
When it happens
Trigger: 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().
Common situations: 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.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- callbackUrl DNS resolution failed: ${message}
- ${errorMessage}
- Cloudflare ${method} ${path} did not complete (a write may s
- Convex embed key validation unavailable: fetch-error
- Convex embed key validation unavailable: invalid-json
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/e10d33959fe2e39a.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seed-natural-events.mjs:363
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,
},
);
}
return parseNhcGeoJson(payload, layerId, expectedGeometryTypes);
}, 1, 500);
}
const NHC_STORM_TYPES = {
HU: 'Hurricane', TS: 'Tropical Storm', TD: 'Tropical Depression',
STS: 'Subtropical Storm', STD: 'Subtropical Depression',
EX: 'Post-Tropical', PT: 'Post-Tropical',
};
View on GitHub (pinned to 7d06c8633d)