yikart/AiToEarn · critical · AppException

ResponseCode.ChannelAccessTokenFailed

ResponseCode.ChannelAccessTokenFailed

Error message

ChannelAccessTokenFailed

What it means

Thrown in AuthService.saveAccountProfile when the CredentialResult for the connected account has no accessToken. The account record may already have been created/updated, but persisting channel credentials requires a non-empty access token, so the flow aborts.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts:281

    source: string
    allowReassign?: boolean
  }): Promise<ConnectedSelectableAccount> {
    const groupId = await this.resolveGroupId(input.userId, input.groupId)
    const account = await this.createOrUpdateAccount({
      userId: input.userId,
      platform: input.platform,
      platformUid: input.profile.platformUid,
      account: input.profile.account,
      displayName: input.profile.displayName,
      avatarUrl: input.profile.avatarUrl,
      fansCount: input.profile.fansCount,
      followingCount: input.profile.followingCount,
      groupId,
      allowReassign: input.allowReassign ?? this.shouldReassignDouyinAccount(input.source, input.platform, input.credential),
    })

    if (!input.credential.accessToken) {
      throw new AppException(ResponseCode.ChannelAccessTokenFailed)
    }

    await this.credentialService.saveCredential(account.id, input.platform, {
      accessToken: input.credential.accessToken,
      refreshToken: input.credential.refreshToken,
      expiresAt: input.credential.expiresAt,
      scope: input.credential.scope,
      raw: input.credential.raw,
    })

    return {
      accountId: account.id,
      platform: input.platform,
      platformUid: input.profile.platformUid,
      account: input.profile.account,
      displayName: input.profile.displayName,
      avatarUrl: input.profile.avatarUrl,
    }

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Inspect the provider adapter's exchangeCode: confirm the access token field is mapped into CredentialResult.accessToken correctly.
  2. Call the provider's token endpoint manually with the code to see whether a token is actually issued (check app secret, redirect_uri, grant_type).
  3. Verify the platform app configuration (client id/secret, redirect URI) — misconfiguration often yields no token.
  4. If the platform defers token exchange, implement the exchange before saveAccountProfile rather than passing an empty credential.

Example fix

// before: adapter drops the token field
return { refreshToken: res.refresh_token, expiresAt } // accessToken missing

// after: map the access token explicitly
return { accessToken: res.access_token, refreshToken: res.refresh_token, expiresAt }
Defensive patterns

Strategy: validation

Validate before calling

function credentialHasAccessToken(c: CredentialResult): boolean {
  return typeof c.accessToken === 'string' && c.accessToken.length > 0
}
if (!credentialHasAccessToken(credentialResult)) {
  throw new Error('Token exchange produced no access token — inspect provider token endpoint response')
}

Type guard

function hasAccessToken(c: CredentialResult): c is CredentialResult & { accessToken: string } {
  return typeof c.accessToken === 'string' && c.accessToken.length > 0
}

Try / catch

try {
  await connectAccountProfile(input)
}
catch (e) {
  if (getErrorCode(e) === ResponseCode.ChannelAccessTokenFailed) {
    logCredentialExchangeDebug(input.platform) // no accessToken from exchangeCode
  }
}

Prevention

When it happens

Trigger: saveAccountProfile (via connectedAccount / completeCallback) receives a credential whose accessToken is empty/undefined — typically a provider adapter that returns a CredentialResult with refreshToken or raw data but no access token after exchangeCode, or a token exchange that silently failed and returned empty fields.

Common situations: Provider token endpoint returning 200 with an error body the adapter maps to an empty token; OAuth scope/app-config problems so the provider issues no token; adapter bug storing the token under a different field name; token exchange skipped for deferred-exchange platforms.

Related errors


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