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
- Ensure the mini-app requests a fresh login code for every code2Session call (codes are single-use)
- Verify clientId/clientSecret match the Douyin mini-app that issued the code
- 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
- Always request a fresh login code per exchange; never retry with a consumed code
- Verify clientId/clientSecret match the mini-app that generated the code
- Log Douyin errcode/errmsg embedded in 200 responses
- Detect client clock skew / stale cached codes in the mini-app client
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
- 15070
- ChannelAccessTokenFailed
- ChannelAuthRefreshTokenMissing
- ChannelAuthPlatformUidMissing
- ChannelAccessTokenFailed
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/9f087fea909a9c73.
Report an issue: GitHub.