{"record":{"id":"d582ed9ecdd2fb65","repo":"yikart/AiToEarn","slug":"channelaccesstokenfailed-d582ed","errorCode":"ChannelAccessTokenFailed","errorMessage":"ChannelAccessTokenFailed","messagePattern":"ChannelAccessTokenFailed","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"critical","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/douyin.service.ts","lineNumber":370,"sourceCode":"        }\n        return cached.access_token\n      }\n    }\n\n    const response = await this.http.post<DouyinOAuthEnvelope<DouyinClientTokenResponse>>(\n      '/oauth/client_token/',\n      {\n        grant_type: DouyinOAuthGrantType.ClientCredential,\n        client_key: this.cfg.clientId,\n        client_secret: this.cfg.clientSecret,\n      },\n      {\n        headers: { 'Content-Type': 'application/json' },\n      },\n    )\n    const result = response.data.data\n    if (!result?.access_token) {\n      throw new AppException(ResponseCode.ChannelAccessTokenFailed, { platform: AccountType.Douyin, field: 'access_token', reasonCode: 'missing_platform_field' })\n    }\n    const expiresIn = Number(result.expires_in)\n    const expiresAt = now + expiresIn * 1000\n\n    this.clientTokenCache = {\n      accessToken: result.access_token,\n      expiresAt,\n    }\n    await this.redis.saveDouyinClientToken({\n      access_token: result.access_token,\n      expires_in: expiresIn,\n      expiresAt,\n    })\n    return result.access_token\n  }\n\n  private async getOpenTicket(): Promise<string> {\n    const clientToken = await this.getClientToken()","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/douyin.service.ts#L352-L388","documentation":"ChannelAccessTokenFailed is thrown by DouyinService.getClientToken when POST /oauth/client_token/ does not return data.access_token. This token (client_credentials grant using client_key/client_secret) is required for all app-level Douyin API calls (share-id, ticket). The error means the credential exchange itself failed, typically because the platform returned an error envelope.","triggerScenarios":"Any first call or cache-miss call to getClientToken (directly or via clientToken/refreshedClientToken after cache invalidation) where Douyin's response lacks data.access_token — e.g. wrong client_key, invalid client_secret, app suspended, or platform error payload.","commonSituations":"DOUYIN_CLIENT_KEY/DOUYIN_CLIENT_SECRET env vars missing, swapped, or rotated on the platform but not on the server; both in-memory and Redis token caches empty on a fresh deployment; Douyin app disabled or under review; clock/network issues producing a malformed response.","solutions":["Check the response body of /oauth/client_token/ for the platform error_code (e.g. 40015 invalid client_secret) and correct DOUYIN_CLIENT_KEY/DOUYIN_CLIENT_SECRET accordingly.","Confirm the Douyin Open Platform app is approved and not suspended, and that the client_key matches an app with the required scopes.","Clear the stale Redis douyin client-token cache key if it holds an expired/rejected token, then retry.","If the secret was rotated, redeploy servers with the new secret before the old one is revoked."],"exampleFix":"// before\nDOUYIN_CLIENT_KEY=aw9x0000000000\nDOUYIN_CLIENT_SECRET=             // empty -> /oauth/client_token/ fails -> throws\n// after\nDOUYIN_CLIENT_KEY=<real client_key>\nDOUYIN_CLIENT_SECRET=<matching client_secret> // response now has data.access_token","handlingStrategy":"validation","validationCode":"function assertDouyinCredentials(cfg: { clientId?: string; clientSecret?: string }) {\n  if (!cfg.clientId || !cfg.clientSecret) throw new Error('DOUYIN_CLIENT_KEY/DOUYIN_CLIENT_SECRET are required')\n}\nassertDouyinCredentials(douyinConfig)","typeGuard":"const hasClientToken = (r: unknown): r is { data: { data: { access_token: string; expires_in: number } } } =>\n  !!r && typeof r === 'object' && typeof (r as any).data?.data?.access_token === 'string'","tryCatchPattern":"try {\n  const token = await douyinService.clientToken()\n} catch (err) {\n  if (err instanceof AppException && err.code === ResponseCode.ChannelAccessTokenFailed) {\n    // reload config, clear Redis token cache, alert ops about credential failure\n  } else throw err\n}","preventionTips":["Validate client_key/client_secret are set and non-empty at service startup","Rotate platform secrets with zero downtime: deploy new secret before revoking the old one","Monitor token fetch failure rate; a spike means credentials or the platform changed","Keep the Redis token cache TTL aligned with expires_in minus a safety buffer"],"tags":["douyin","oauth","client-token","credentials"],"backgroundTag":"oauth-client-credentials-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}