{"record":{"id":"8d5421223d3b5d78","repo":"chatboxai/chatbox","slug":"license-validation-response-was-not-understood","errorCode":null,"errorMessage":"License validation response was not understood","messagePattern":"License validation response was not understood","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/shared/services/native-license.ts","lineNumber":86,"sourceCode":"    error: data?.error,\n  }\n}\n\nexport async function validateNativeLicense(\n  licenseKey: string,\n  instanceId: string,\n  options: NativeLicenseRequestOptions = {}\n): Promise<boolean> {\n  const payload = (await postLicense('/api/license/validate', { licenseKey, instanceId }, options)) as {\n    data?: { valid?: boolean }\n  } | null\n  // Only an explicit boolean is authoritative. A garbage 200 response (captive\n  // portal / proxy HTML / empty body) must NOT be read as \"invalid\": the caller\n  // (premiumActions.useAutoValidate) clears the user's license on `valid === false`,\n  // so an indeterminate response has to throw and be treated like a network error\n  // (license kept), matching the old remote.validateLicense which threw on res.json().\n  if (typeof payload?.data?.valid !== 'boolean') {\n    throw new Error('License validation response was not understood')\n  }\n  return payload.data.valid\n}\n\nexport async function deactivateNativeLicense(\n  licenseKey: string,\n  instanceId: string,\n  options: NativeLicenseRequestOptions = {}\n): Promise<void> {\n  await postLicense('/api/license/deactivate', { licenseKey, instanceId }, options)\n}\n\nexport interface NativeLicenseDetailResult {\n  detail: ChatboxAILicenseDetail | null\n  error?: { code?: string; message?: string }\n}\n\ntype NativeLicenseDetailPayload = { data?: unknown; error?: { code?: string; message?: string } }","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/shared/services/native-license.ts#L68-L104","documentation":"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().","triggerScenarios":"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.","commonSituations":"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.","solutions":["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).","Verify options.apiOrigin resolves to the real Chatbox API (default https://api.chatboxai.app) and is not behind a captive portal or rewriting proxy.","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.","If you operate a custom apiOrigin/server, return exactly { \"data\": { \"valid\": true } } (or false) and never a 200 with an empty or HTML body.","Check for a backend/schema version mismatch if the issue appears only after an app or server upgrade."],"exampleFix":"// before (caller treats unknown as invalid -> wipes license)\ntry {\n  const ok = await validateNativeLicense(key, instanceId)\n  if (!ok) clearLicense()\n} catch (e) {\n  throw e\n}\n\n// after (indeterminate response is network-equivalent -> keep license)\ntry {\n  const ok = await validateNativeLicense(key, instanceId)\n  if (!ok) clearLicense()\n} catch (e) {\n  // HTTP error OR indeterminate 200: preserve the license, retry later\n  console.warn('license validation unavailable, keeping current license', e)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// Narrow a parsed validate payload before trusting it\nfunction isLicenseValidatePayload(\n  v: unknown\n): v is { data: { valid: boolean } } {\n  return (\n    !!v &&\n    typeof v === 'object' &&\n    'data' in v &&\n    !!((v as any).data) &&\n    typeof (v as any).data.valid === 'boolean'\n  )\n}","tryCatchPattern":"// Indeterminate 200 == network error: keep the license, do not clear\ntry {\n  const ok = await validateNativeLicense(licenseKey, instanceId, { signal })\n  if (!ok) clearLicense()\n} catch (e) {\n  // thrown by either transport failure or an unparseable 200 body\n  keepLicenseAndRetryLater()\n}","preventionTips":["Never translate this thrown error into valid === false; the guard exists precisely to protect the license.","Inject the renderer's retrying afetch as fetchFn so transient transport errors retry before reaching this code.","Do not override apiOrigin to an untrusted host that may return captive-portal HTML.","Log the raw status/body when this fires once to confirm whether it is a schema change versus a network interception."],"tags":["license","api-contract","network","defensive","validation"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}