chatboxai/chatbox · error · Error

License validation response was not understood

Error message

License validation response was not understood

What it means

Thrown by validateNativeLicense (src/shared/services/native-license.ts:86) when POST /api/license/validate returns HTTP 2xx but the body cannot be confirmed as { data: { valid: boolean } }. postLicense already parses the body in a try/catch and yields null on non-JSON, so a captive-portal page, proxy-injected HTML, or empty body makes payload?.data?.valid undefined. The guard is deliberate and load-bearing: the caller premiumActions.useAutoValidate clears the user's license on valid === false, so an indeterminate 200 must throw and be treated like a network error (license kept), mirroring the old remote.validateLicense which threw on res.json().

Source

Thrown at src/shared/services/native-license.ts:86

    error: data?.error,
  }
}

export async function validateNativeLicense(
  licenseKey: string,
  instanceId: string,
  options: NativeLicenseRequestOptions = {}
): Promise<boolean> {
  const payload = (await postLicense('/api/license/validate', { licenseKey, instanceId }, options)) as {
    data?: { valid?: boolean }
  } | null
  // Only an explicit boolean is authoritative. A garbage 200 response (captive
  // portal / proxy HTML / empty body) must NOT be read as "invalid": the caller
  // (premiumActions.useAutoValidate) clears the user's license on `valid === false`,
  // so an indeterminate response has to throw and be treated like a network error
  // (license kept), matching the old remote.validateLicense which threw on res.json().
  if (typeof payload?.data?.valid !== 'boolean') {
    throw new Error('License validation response was not understood')
  }
  return payload.data.valid
}

export async function deactivateNativeLicense(
  licenseKey: string,
  instanceId: string,
  options: NativeLicenseRequestOptions = {}
): Promise<void> {
  await postLicense('/api/license/deactivate', { licenseKey, instanceId }, options)
}

export interface NativeLicenseDetailResult {
  detail: ChatboxAILicenseDetail | null
  error?: { code?: string; message?: string }
}

type NativeLicenseDetailPayload = { data?: unknown; error?: { code?: string; message?: string } }

View on GitHub (pinned to 81571269ad)

Solutions

  1. Treat the thrown error as transient in the caller: keep the existing license and surface a network-style message. Do NOT map it to valid === false (that would wipe a good license).
  2. Verify options.apiOrigin resolves to the real Chatbox API (default https://api.chatboxai.app) and is not behind a captive portal or rewriting proxy.
  3. Reproduce the raw exchange: curl -i -X POST '<origin>/api/license/validate' -H 'Content-Type: application/json' -d '{"licenseKey":"...","instanceId":"..."}' and inspect whether the body is JSON with data.valid as a real boolean.
  4. If you operate a custom apiOrigin/server, return exactly { "data": { "valid": true } } (or false) and never a 200 with an empty or HTML body.
  5. Check for a backend/schema version mismatch if the issue appears only after an app or server upgrade.

Example fix

// before (caller treats unknown as invalid -> wipes license)
try {
  const ok = await validateNativeLicense(key, instanceId)
  if (!ok) clearLicense()
} catch (e) {
  throw e
}

// after (indeterminate response is network-equivalent -> keep license)
try {
  const ok = await validateNativeLicense(key, instanceId)
  if (!ok) clearLicense()
} catch (e) {
  // HTTP error OR indeterminate 200: preserve the license, retry later
  console.warn('license validation unavailable, keeping current license', e)
}
Defensive patterns

Strategy: try-catch

Type guard

// Narrow a parsed validate payload before trusting it
function isLicenseValidatePayload(
  v: unknown
): v is { data: { valid: boolean } } {
  return (
    !!v &&
    typeof v === 'object' &&
    'data' in v &&
    !!((v as any).data) &&
    typeof (v as any).data.valid === 'boolean'
  )
}

Try / catch

// Indeterminate 200 == network error: keep the license, do not clear
try {
  const ok = await validateNativeLicense(licenseKey, instanceId, { signal })
  if (!ok) clearLicense()
} catch (e) {
  // thrown by either transport failure or an unparseable 200 body
  keepLicenseAndRetryLater()
}

Prevention

When it happens

Trigger: POST {apiOrigin}/api/license/validate resolves with response.ok === true, but either (a) response.json() throws inside postLicense so payload is null, or (b) payload.data is missing, or (c) payload.data.valid is present but not a strict boolean (e.g. the server sent a string "true" or null). All three fail the typeof === 'boolean' check at line 85.

Common situations: Captive WiFi portal intercepting the request and returning HTML with status 200; a corporate proxy rewriting the response body; a CDN/WAF serving a soft error page with 200; the Chatbox backend deploying a schema change that drops or renames data.valid; an empty body from a degraded gateway; a custom apiOrigin pointing at a mock/stub that returns a different envelope.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/8d5421223d3b5d78. Report an issue: GitHub.