yikart/AiToEarn · error · AppException

ChannelAccessTokenFailed

ChannelAccessTokenFailed

Error message

ChannelAccessTokenFailed

What it means

code2Session exchanges a mini-app login code with Douyin and throws ChannelAccessTokenFailed when the response body lacks data.openid. Without openid the session cannot be identified, so the exchange is treated as failed authentication.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/douyin-miniapp.service.ts:99

  async code2Session(code: string): Promise<DouyinMiniAppSessionInfo> {
    const miniApp = this.miniAppConfig
    const response = await this.http.post<DouyinMiniAppCode2SessionResponse>(
      this.endpoints.code2Session,
      {
        appid: miniApp.clientId,
        secret: miniApp.clientSecret,
        code,
      },
      {
        headers: {
          'Content-Type': 'application/json',
        },
      },
    )
    const data = response.data.data
    if (!data?.openid) {
      throw new AppException(ResponseCode.ChannelAccessTokenFailed, { platform: AccountType.Douyin, field: 'openid', reasonCode: 'missing_platform_field' })
    }

    return {
      openid: data.openid,
      unionid: data.unionid,
      anonymousOpenid: data.anonymous_openid ?? data.anonymousOpenid,
    }
  }

  async getUserAccessToken(ticket: string): Promise<DouyinMiniAppAccessTokenInfo> {
    const miniApp = this.miniAppConfig
    const response = await this.http.post<DouyinMiniAppAccessTokenResponse>(
      this.endpoints.userAccessToken,
      new URLSearchParams({
        client_key: miniApp.clientId,
        client_secret: miniApp.clientSecret,
        code: ticket,
        grant_type: DouyinOAuthGrantType.AuthorizationCode,

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Ensure the mini-app requests a fresh login code for every code2Session call (codes are single-use)
  2. Verify clientId/clientSecret match the Douyin mini-app that issued the code
  3. Log response.data to surface Douyin's errcode/errmsg, which explains why openid is missing

Example fix

// before
if (!data?.openid) {
  throw new AppException(ResponseCode.ChannelAccessTokenFailed, { platform: AccountType.Douyin, field: 'openid', reasonCode: 'missing_platform_field' })
}
// after
if (!data?.openid) {
  throw new AppException(ResponseCode.ChannelAccessTokenFailed, { platform: AccountType.Douyin, field: 'openid', reasonCode: 'missing_platform_field', douyinErrcode: data?.errcode, douyinErrmsg: data?.errmsg })
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!code || typeof code !== 'string') {
  throw new Error('code2Session requires a fresh, non-empty js_code from the mini-app client')
}

Type guard

function hasOpenid(res: { data?: { data?: { openid?: unknown } } }): res is { data: { data: { openid: string } } } {
  return typeof res.data?.data?.openid === 'string' && res.data.data.openid.length > 0
}

Try / catch

try {
  const session = await service.code2Session(code)
} catch (e) {
  if (e instanceof AppException && e.code === 'ChannelAccessTokenFailed') {
    // codes are single-use: have the client request a new login code
    return { retryWithNewCode: true }
  }
  throw e
}

Prevention

When it happens

Trigger: Calling code2Session with an expired, already-used, or invalid js_code, or Douyin returning 200 with an error body containing no openid (e.g. invalid code, appid/secret mismatch).

Common situations: Mini-app client cached/replayed a login code (codes are single-use), clientId/clientSecret configured for the wrong Douyin app, clock/network issues producing stale codes, or Douyin-side error returned inside a 200 body.

Related errors


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