{"record":{"id":"ce88a37e6e38b150","repo":"koala73/worldmonitor","slug":"imd-auth-response-invalid","errorCode":null,"errorMessage":"IMD_AUTH_RESPONSE_INVALID","messagePattern":"IMD_AUTH_RESPONSE_INVALID","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/imd-cyclone-marine.mjs","lineNumber":839,"sourceCode":"  try {\n    const response = await fetchFn(IMD_OAUTH_TOKEN_URL, {\n      method: 'POST',\n      headers: {\n        Accept: 'application/json',\n        'Content-Type': 'application/json',\n        'User-Agent': userAgent,\n      },\n      body: JSON.stringify({ email, password }),\n      redirect: 'error',\n      signal: AbortSignal.timeout(timeoutMs),\n    });\n    if (!response.ok) throw new Error(`IMD_AUTH_HTTP_${response.status}`);\n    let payload;\n    try {\n      payload = await readBoundedJsonResponse(response, maxBytes);\n    } catch (err) {\n      if (/^IMD_RESPONSE_TOO_LARGE:\\d+$/.test(String(err?.message || ''))) throw err;\n      throw new Error('IMD_AUTH_RESPONSE_INVALID');\n    }\n    const accessToken = typeof payload?.access_token === 'string' ? payload.access_token : '';\n    const tokenType = typeof payload?.token_type === 'string' ? payload.token_type.trim() : '';\n    const expiresIn = Number(payload?.expires_in);\n    if (\n      !/^[\\u0021-\\u007E]+$/.test(accessToken)\n      || tokenType.toLowerCase() !== 'bearer'\n      || !Number.isFinite(expiresIn)\n      || expiresIn <= 0\n    ) {\n      throw new Error('IMD_AUTH_RESPONSE_INVALID');\n    }\n    return { token: accessToken, error: null };\n  } catch (err) {\n    return { token: null, error: imdAuthFailureReason(err) };\n  }\n}\n","sourceCodeStart":821,"sourceCodeEnd":857,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/lib/imd-cyclone-marine.mjs#L821-L857","documentation":"Thrown by mintImdApiToken when the auth endpoint returned HTTP 2xx but readBoundedJsonResponse could not parse a bounded JSON body. The only parse-stage error that passes through unchanged is IMD_RESPONSE_TOO_LARGE:<bytes>; everything else (invalid JSON, empty body, truncated response, non-JSON content type) is normalized to IMD_AUTH_RESPONSE_INVALID. imdAuthFailureReason maps it 1:1 to the caller-facing failure reason.","triggerScenarios":"IMD_OAUTH_TOKEN_URL returns 200 with a body that is not valid JSON, an empty body, HTML (e.g. a login page or WAF challenge), or a body larger than maxBytes that fails the bounded read with an error other than the RESPONSE_TOO_LARGE marker.","commonSituations":"IMD behind a CDN/WAF serving an HTML challenge page with status 200; IMD API contract change (new response format); captive proxy injecting content; truncated gzip/plain body from a flaky connection.","solutions":["Log the raw response body (within bounds) for one failing request to see what the endpoint actually returned.","If it's HTML/WAF challenge content, the request needs different headers or must come from an allowlisted IP.","If the API contract changed, update the payload parsing to match the new format.","If the body is intermittently truncated, increase timeoutMs or retry; persistent truncation suggests a proxy issue.","Ensure maxBytes is left at the default (IMD_MAX_BYTES) unless a token response legitimately exceeds it — then handle the RESPONSE_TOO_LARGE path instead."],"exampleFix":"// before\npayload = await readBoundedJsonResponse(response, maxBytes);\n// after\nconst text = await response.text();\ntry { payload = JSON.parse(text); }\ncatch { console.error('IMD auth body head:', text.slice(0, 200)); throw new Error('IMD_AUTH_RESPONSE_INVALID'); }","handlingStrategy":"retry","validationCode":"null","typeGuard":"function isOAuthTokenPayload(p) { return p !== null && typeof p === 'object' && typeof p.access_token === 'string' && p.access_token.length > 0 && typeof p.token_type === 'string' && typeof p.expires_in === 'number' && Number.isFinite(p.expires_in) && p.expires_in > 0; }","tryCatchPattern":"const { token, error } = await mintImdApiToken({ email, password });\nif (error === 'IMD_AUTH_RESPONSE_INVALID') {\n  // inspect body once, then retry with backoff in case of truncated/CDN response\n  await sleep(RETRY_MS);\n  return mintImdApiToken({ email, password });\n}","preventionTips":["Log a bounded head of unexpected bodies (first ~200 chars) for diagnosis, redacting tokens.","Watch for IMD/WAF contract changes after their deployments; alert on the first INVALID occurrence.","Keep maxBytes at the library default so size handling follows the intended RESPONSE_TOO_LARGE path.","Prefer retrying transiently over failing the whole cycle for a single bad response."],"tags":["auth","json","response-parsing"],"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"}