coreyhaines31/marketingskills · error · Error

Authentication failed: ${text}

Error message

Authentication failed: ${text}

What it means

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.

Source

Thrown at tools/clis/zoominfo.js:35

    throw new Error('ZOOMINFO_USERNAME and ZOOMINFO_PRIVATE_KEY required for authentication')
  }
  const res = await fetch(`${BASE_URL}/authenticate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  })
  const text = await res.text()
  if (!res.ok) {
    throw new Error(`Authentication failed (${res.status}): ${text}`)
  }
  try {
    const data = JSON.parse(text)
    if (!data.jwt) throw new Error('No JWT in response')
    ACCESS_TOKEN = data.jwt
    return ACCESS_TOKEN
  } catch (e) {
    if (e.message === 'No JWT in response') throw e
    throw new Error(`Authentication failed: ${text}`)
  }
}

async function api(method, path, body) {
  if (args['dry-run']) {
    return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ***' }, body }
  }
  const token = await authenticate()
  const res = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: body ? JSON.stringify(body) : undefined,
  })
  const text = await res.text()
  try {

View on GitHub (pinned to 7868cb9251)

Solutions

  1. Inspect the text value embedded in the message to identify the actual body (HTML title, proxy banner, etc.).
  2. Verify direct connectivity with a raw request: `curl -i https://api.zoominfo.com/authenticate` and confirm a JSON content-type.
  3. Check for an intercepting proxy, VPN, or DNS override rerouting api.zoominfo.com.
Defensive patterns

Strategy: try-catch

Try / catch

let data
try {
  data = JSON.parse(text)
} catch (e) {
  const looksLikeHtml = /^\s*<(?:!doctype|html|body)/i.test(text)
  if (looksLikeHtml) {
    throw new Error('ZoomInfo auth returned an HTML page -- likely a proxy/VPN block page (check connectivity)')
  }
  throw new Error(`ZoomInfo auth response was not valid JSON (first 200 chars): ${text.slice(0, 200)}`)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of coreyhaines31/marketingskills@7868cb9251 (2026-08-13). Data as JSON: /api/errors/fd02cc32f3eb0b4d. Report an issue: GitHub.