yikart/AiToEarn · error · AppException

body.code

body.code

Error message

Relay API request failed

What it means

unwrapResponse normalizes Relay API responses: if the body is a RelayCommonResponse envelope and carries a non-success code, it throws AppException(body.code, body.message ?? 'Relay API request failed'). The message shown ('Relay API request failed') is the fallback used when the upstream body has a failure code but no message — the real information is in the exception's code (body.code).

Source

Thrown at project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/relay/relay.service.ts:58

    const data = error.response?.data as Record<string, unknown> | undefined
    const message = (data?.['message'] as string) || (data?.['error'] as string) || error.message
    error.message = message
    error.name = status ? `RelayApiError(${status})` : 'RelayApiError'
    return error
  }

  private normalizeResponse<T>(response: AxiosResponse<RelayCommonResponse<T> | T>): AxiosResponse<T> {
    response.data = this.unwrapResponse(response.data)
    return response as AxiosResponse<T>
  }

  private unwrapResponse<T>(body: RelayCommonResponse<T> | T): T {
    if (!this.isCommonResponse(body)) {
      return body as T
    }

    if (body.code != null && body.code !== ResponseCode.Success) {
      throw new AppException(body.code, body.message ?? 'Relay API request failed')
    }

    return body.data as T
  }

  private isCommonResponse<T>(body: RelayCommonResponse<T> | T): body is RelayCommonResponse<T> {
    return typeof body === 'object'
      && body !== null
      && 'data' in body
  }

  private stringifyForError(value: unknown): string {
    try {
      return JSON.stringify(value)
    }
    catch {
      return String(value)
    }

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Read the AppException code (body.code) to identify the specific relay failure; handle by code, not the generic message
  2. Verify RELAY_API_KEY matches the RELAY_SERVER_URL environment (aitoearn.cn key ↔ https://aitoearn.cn/api, aitoearn.ai key ↔ https://aitoearn.ai/api)
  3. Check relay account balance/quota in the corresponding environment
  4. Log the full relay response body to capture body.message when present

Example fix

// before
const result = await relayService.normalizeResponse(...)
// after
try {
  const result = await relayService.normalizeResponse(...)
} catch (e) {
  if (e instanceof AppException && e.code === ResponseCode.Unauthorized) {
    throw new UnauthorizedException('Relay API key invalid or env mismatch (cn vs ai)')
  }
  throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

function relayEnvMatches(apiKey: string, serverUrl: string): boolean {
  const isCn = serverUrl.includes('aitoearn.cn')
  const keyLooksCn = /* per deployment convention */ true
  return isCn === keyLooksCn // cn keys must pair with aitoearn.cn, intl keys with aitoearn.ai
}

Type guard

function isRelayError(e: unknown): e is AppException {
  return e instanceof AppException && e.code !== ResponseCode.Success
}

Try / catch

try {
  const data = await relayService.normalizeResponse(body)
} catch (e) {
  if (e instanceof AppException) {
    logger.error({ relayCode: e.code, relayMessage: e.message }, 'relay failure')
    throw new BadGatewayException(`Relay error ${e.code}: ${e.message}`)
  }
  throw e
}

Prevention

When it happens

Trigger: The relay upstream (Relay server /api/ai/...) responds with its common error envelope: invalid RELAY_API_KEY, insufficient quota/balance, upstream provider error proxied through, or bad request params rejected by the relay. Any call flowing through normalizeResponse can surface it.

Common situations: Key/env mismatch (China aitoearn.cn key used against aitoearn.ai relay or vice versa → 401-style codes); exhausted credit; relay forwarding a provider-side failure; deprecated endpoint params.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/e7797d552c338917. Report an issue: GitHub.