hcengineering/platform · error · PaymentError

Payment service error: ${response.status} ${text}

Error message

Payment service error: ${response.status} ${text}

What it means

fetchSafe throws `Payment service error: ${response.status} ${text}` when the response is non-OK and its body does NOT parse as valid JSON (the JSON-parse attempt threw, so the catch branch runs). It bundles the HTTP status code and the raw body text into a PaymentError so the caller can still see what went wrong. This is the unstructured-response counterpart of the structured PaymentError path.

Source

Thrown at packages/payment-client/src/client.ts:170

 * @returns Response
 * @throws NetworkError on network issues
 * @throws PaymentError on non-ok responses
 */
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: ${String(err)}`)
  }

  if (!response.ok) {
    const text = await response.text()
    try {
      const error = JSON.parse(text)
      throw new PaymentError(error.error ?? text)
    } catch {
      throw new PaymentError(`Payment service error: ${response.status} ${text}`)
    }
  }

  return response
}

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Inspect the status code and raw text in the message — HTML bodies usually indicate a proxy/gateway problem rather than the payment app itself.
  2. Check whether the payment service is up and whether its reverse proxy/load balancer is healthy.
  3. Verify the baseUrl isn't pointing at the wrong host (e.g. a proxy instead of the API).
  4. Retry with backoff if the status is 5xx, and alert if it persists.

Example fix

// before
const res = await paymentClient.response('/charge', init)
// after
try {
  const res = await paymentClient.response('/charge', init)
} catch (e) {
  if (e instanceof PaymentError && /Payment service error: 5\d\d/.test(e.message)) {
    // gateway/server outage — retry with backoff
  } else throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: ensure the service returns JSON (not a proxy page) before real calls
const health = await fetch(baseUrl + '/health')
const ct = health.headers.get('content-type') ?? ''
if (!ct.includes('application/json')) throw new Error('Payment endpoint not serving JSON — check proxy/baseUrl')

Type guard

function isGatewayStyleError(e: unknown): boolean {
  return e instanceof PaymentError && /Payment service error: (502|503|504)\b/.test(e.message)
}

Try / catch

try {
  await paymentClient.response('/charge', init)
} catch (e) {
  if (isGatewayStyleError(e)) {
    // 5xx with non-JSON body: proxy/gateway outage — retry with backoff or alert ops
  } else throw e
}

Prevention

When it happens

Trigger: Any PaymentClient call (via `response` → fetchSafe) receiving 4xx/5xx with an HTML/plain-text/empty body — e.g. an nginx 502 Bad Gateway page or a bare 'Internal Server Error' string.

Common situations: Payment service behind a reverse proxy that failed to reach the upstream (502/504), service crash returning HTML error page, load balancer maintenance page, or request blocked by a WAF returning HTML.

Related errors


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