hcengineering/platform · error · BillingError

BillingError: server response text

Error message

BillingError: server response text

What it means

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.

Source

Thrown at packages/billing-client/src/client.ts:123

    const path = '/api/v1/ai/tokens'
    const url = new URL(concatLink(this.endpoint, path))
    const body = JSON.stringify(data)

    await fetchSafe(url, { method: 'POST', headers: { ...this.headers }, body })
  }
}

async function fetchSafe (url: string | URL, init?: RequestInit): Promise<Response> {
  let response
  try {
    response = await fetch(url, init)
  } catch (err: any) {
    throw new NetworkError(`Network error ${err}`)
  }

  if (!response.ok) {
    const text = await response.text()
    throw new BillingError(text)
  }

  return response
}

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Read the BillingError message (response text) — it usually contains the server's error JSON explaining the failure.
  2. For 401/403, refresh the billing token passed to getClient.
  3. For 400, validate the request payload sent to the failing method.
  4. For 5xx, retry with backoff and check billing service health/logs.

Example fix

// before
const stats = await client.response(workspace) // BillingError: {"message":"unauthorized"}
// after
try {
  const stats = await client.response(workspace)
} catch (e) {
  if (e instanceof BillingError && /unauthorized/i.test(e.message)) {
    client = getClient(url, await refreshBillingToken())
  } else throw e
}
Defensive patterns

Strategy: try-catch

Type guard

function isBillingError(e: unknown): e is BillingError {
  return e instanceof BillingError
}

Try / catch

try {
  return await client.response(ws)
} catch (e) {
  if (isBillingError(e)) {
    const body = e.message
    if (/unauthorized|forbidden/i.test(body)) await refreshToken()
    else if (/^4/.test(statusFrom(body))) throw new Error(`bad request: ${body}`)
    else throw e // 5xx -> alert/monitor
  }
  throw e
}

Prevention

When it happens

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

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

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/ab87339cea5edb2e. Report an issue: GitHub.