{"record":{"id":"fd02cc32f3eb0b4d","repo":"coreyhaines31/marketingskills","slug":"authentication-failed-text","errorCode":null,"errorMessage":"Authentication failed: ${text}","messagePattern":"Authentication failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/clis/zoominfo.js","lineNumber":35,"sourceCode":"    throw new Error('ZOOMINFO_USERNAME and ZOOMINFO_PRIVATE_KEY required for authentication')\n  }\n  const res = await fetch(`${BASE_URL}/authenticate`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ username, password }),\n  })\n  const text = await res.text()\n  if (!res.ok) {\n    throw new Error(`Authentication failed (${res.status}): ${text}`)\n  }\n  try {\n    const data = JSON.parse(text)\n    if (!data.jwt) throw new Error('No JWT in response')\n    ACCESS_TOKEN = data.jwt\n    return ACCESS_TOKEN\n  } catch (e) {\n    if (e.message === 'No JWT in response') throw e\n    throw new Error(`Authentication failed: ${text}`)\n  }\n}\n\nasync function api(method, path, body) {\n  if (args['dry-run']) {\n    return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ***' }, body }\n  }\n  const token = await authenticate()\n  const res = await fetch(`${BASE_URL}${path}`, {\n    method,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${token}`,\n    },\n    body: body ? JSON.stringify(body) : undefined,\n  })\n  const text = await res.text()\n  try {","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/coreyhaines31/marketingskills/blob/7868cb9251fad80a73d26e488a5ad5f6c4a9f335/tools/clis/zoominfo.js#L17-L53","documentation":"The /authenticate call returned a 2xx status but res.text() could not be parsed as JSON (JSON.parse threw SyntaxError, which is then re-wrapped here; the 'No JWT in response' case is re-thrown untouched). It means the success-status body was not JSON at all -- an HTML gateway page, an empty body, or plain text.","triggerScenarios":"A 200 response carrying an HTML block/error page from a corporate proxy or CDN, an empty response body, a plain-text token without a JSON wrapper, or a BASE_URL typo that reaches a different host returning HTML.","commonSituations":"Corporate proxy/VPN injecting an HTML captive-portal or block page with HTTP 200, a ZoomInfo maintenance page served as HTML, or a misconfigured BASE_URL hitting a web frontend rather than the API host.","solutions":["Inspect the text value embedded in the message to identify the actual body (HTML title, proxy banner, etc.).","Verify direct connectivity with a raw request: `curl -i https://api.zoominfo.com/authenticate` and confirm a JSON content-type.","Check for an intercepting proxy, VPN, or DNS override rerouting api.zoominfo.com."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let data\ntry {\n  data = JSON.parse(text)\n} catch (e) {\n  const looksLikeHtml = /^\\s*<(?:!doctype|html|body)/i.test(text)\n  if (looksLikeHtml) {\n    throw new Error('ZoomInfo auth returned an HTML page -- likely a proxy/VPN block page (check connectivity)')\n  }\n  throw new Error(`ZoomInfo auth response was not valid JSON (first 200 chars): ${text.slice(0, 200)}`)\n}","preventionTips":["Whitelist api.zoominfo.com on any corporate proxy/VPN to avoid injected HTML block pages.","Verify the response Content-Type is application/json before parsing.","Confirm BASE_URL has no typo that could reach a non-API host serving HTML."],"tags":["authentication","parsing","network","zoominfo"],"backgroundTag":null,"analyzedSha":"7868cb9251fad80a73d26e488a5ad5f6c4a9f335","analyzedAt":"2026-08-13T03:33:17.303Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}