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
- Inspect the provider adapter's exchangeCode: confirm the access token field is mapped into CredentialResult.accessToken correctly.
- 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).
- Verify the platform app configuration (client id/secret, redirect URI) — misconfiguration often yields no token.
- 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
- Schema-validate provider token responses (access_token must be a non-empty string)
- Verify client secret/redirect_uri/grant_type per platform app config
- Add adapter unit tests asserting accessToken mapping
- Alert on empty access_token fields in provider responses
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
- ResponseCode.ChannelAuthPlatformUidMissing
- No response from Gemini
- ResponseCode.ChannelAuthSessionInvalid
- ResponseCode.ChannelAuthPlatformMismatch
- ResponseCode.ChannelAuthSessionCompleted
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/f7ae174001bff07a.
Report an issue: GitHub.