{"record":{"id":"ab87339cea5edb2e","repo":"hcengineering/platform","slug":"billingerror-server-response-text","errorCode":null,"errorMessage":"BillingError: server response text","messagePattern":"BillingError: server response text","errorType":"exception","errorClass":"BillingError","httpStatus":null,"severity":"error","filePath":"packages/billing-client/src/client.ts","lineNumber":123,"sourceCode":"    const path = '/api/v1/ai/tokens'\n    const url = new URL(concatLink(this.endpoint, path))\n    const body = JSON.stringify(data)\n\n    await fetchSafe(url, { method: 'POST', headers: { ...this.headers }, body })\n  }\n}\n\nasync function fetchSafe (url: string | URL, init?: RequestInit): Promise<Response> {\n  let response\n  try {\n    response = await fetch(url, init)\n  } catch (err: any) {\n    throw new NetworkError(`Network error ${err}`)\n  }\n\n  if (!response.ok) {\n    const text = await response.text()\n    throw new BillingError(text)\n  }\n\n  return response\n}\n","sourceCodeStart":105,"sourceCodeEnd":128,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/packages/billing-client/src/client.ts#L105-L128","documentation":"fetchSafe treats a completed HTTP response with a non-ok status as a BillingError carrying the raw response body text. Unlike NetworkError, the server was reached and returned an error status (4xx/5xx), so the message is whatever the billing server returned — often a JSON error body or plain-text message.","triggerScenarios":"Any fetchSafe-based call (response, postLiveKitSessions, postLiveKitEgress, postAiTranscriptData, postAiTokensData) receiving 400/401/403/404/500 etc. from the billing service — e.g. invalid token, malformed payload, or server-side failure.","commonSituations":"Expired or invalid billing token (401); wrong request shape rejected with 400; billing backend crash or maintenance returning 5xx; hitting a proxy that returns HTML error pages.","solutions":["Read the BillingError message (response text) — it usually contains the server's error JSON explaining the failure.","For 401/403, refresh the billing token passed to getClient.","For 400, validate the request payload sent to the failing method.","For 5xx, retry with backoff and check billing service health/logs."],"exampleFix":"// before\nconst stats = await client.response(workspace) // BillingError: {\"message\":\"unauthorized\"}\n// after\ntry {\n  const stats = await client.response(workspace)\n} catch (e) {\n  if (e instanceof BillingError && /unauthorized/i.test(e.message)) {\n    client = getClient(url, await refreshBillingToken())\n  } else throw e\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isBillingError(e: unknown): e is BillingError {\n  return e instanceof BillingError\n}","tryCatchPattern":"try {\n  return await client.response(ws)\n} catch (e) {\n  if (isBillingError(e)) {\n    const body = e.message\n    if (/unauthorized|forbidden/i.test(body)) await refreshToken()\n    else if (/^4/.test(statusFrom(body))) throw new Error(`bad request: ${body}`)\n    else throw e // 5xx -> alert/monitor\n  }\n  throw e\n}","preventionTips":["Catch BillingError separately from NetworkError — the server responded","Parse/log the response text in the error; it explains the server-side cause","Keep billing tokens fresh to avoid 401-driven BillingErrors","Validate payloads client-side to avoid 400 responses"],"tags":["http","api-error","billing"],"backgroundTag":"http-error-response","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}