{"record":{"id":"e2f4ec3088467f9c","repo":"yikart/AiToEarn","slug":"channelplatformapifailed-e2f4ec","errorCode":"ChannelPlatformApiFailed","errorMessage":"ChannelPlatformApiFailed","messagePattern":"ChannelPlatformApiFailed","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/rednote/offline-qr/rednote-offline-qr.service.ts","lineNumber":59,"sourceCode":"\n    try {\n      const response = await axios.post<RedNoteAccessTokenResponse>(\n        this.config.accessTokenUrl,\n        {\n          app_key: this.config.appKey,\n          nonce,\n          timestamp,\n          signature,\n        },\n        {\n          headers: { 'Content-Type': 'application/json' },\n          timeout: 10000,\n        },\n      )\n\n      const accessToken = response.data?.data?.access_token\n      if (!response.data?.success || !accessToken) {\n        throw new AppException(ResponseCode.ChannelPlatformApiFailed, {\n          platform: AccountType.RedNote,\n          reason: response.data?.msg,\n        })\n      }\n\n      return accessToken\n    }\n    catch (error) {\n      if (error instanceof AppException) {\n        throw error\n      }\n      throw new AppException(ResponseCode.ChannelPlatformApiFailed, {\n        platform: AccountType.RedNote,\n      })\n    }\n  }\n\n  private generateNonce(length = 10): string {","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/rednote/offline-qr/rednote-offline-qr.service.ts#L41-L77","documentation":"fetchAccessToken calls RedNote's OAuth API and expects response.data.success plus a data.access_token. When the platform responds (within the 10s timeout) but reports success=false or omits access_token, this ChannelPlatformApiFailed is thrown with the platform (RedNote) and the platform's msg as reason. It signals an upstream RedNote authorization API rejection, not a local bug.","triggerScenarios":"Calling accessToken/fetchAccessToken for RedNote when the upstream API returns { success: false, msg: ... } or returns 200 without data.access_token — e.g. invalid app_id/app_secret, invalid or expired auth code, revoked grant, or RedNote-side error payload.","commonSituations":"Wrong RedNote client credentials in env config; exchanging an already-used or expired authorization code; RedNote app not approved/whitelisted for the token endpoint; RedNote API contract change so access_token moved out of data.data.","solutions":["Read the `reason` (response.data.msg) field in the exception payload — it contains RedNote's own error message identifying the rejection cause.","Verify the RedNote client key/secret and grant_type configured for the token request are correct and active in the RedNote open platform console.","Ensure the authorization code being exchanged is fresh and single-use; re-run the OAuth flow to obtain a new code.","Log the full response body once to confirm the response shape still matches response.data.data.access_token; update parsing if the platform changed its schema."],"exampleFix":"// before\nconst accessToken = response.data?.data?.access_token\nif (!response.data?.success || !accessToken) { throw ... }\n// after\nconst accessToken = response.data?.data?.access_token\nif (!accessToken) {\n  this.logger.error(`RedNote token exchange failed: ${JSON.stringify(response.data)}`)\n  throw new AppException(ResponseCode.ChannelPlatformApiFailed, {\n    platform: AccountType.RedNote,\n    reason: response.data?.msg ?? 'missing access_token',\n  })\n}","handlingStrategy":"try-catch","validationCode":"// No local pre-check can fully prevent upstream rejection; verify inputs before exchange\nif (!authCode) throw new Error('Missing RedNote authorization code')\nif (!process.env.REDNOTE_CLIENT_KEY || !process.env.REDNOTE_CLIENT_SECRET) {\n  throw new Error('RedNote client credentials not configured')\n}","typeGuard":"function hasAccessToken(res: unknown): res is { success: boolean; data: { access_token: string } } {\n  return !!res && typeof res === 'object'\n    && 'success' in res && (res as any).success === true\n    && !!(res as any).data?.access_token\n}","tryCatchPattern":"try {\n  await rednoteService.accessToken(...)\n} catch (e) {\n  if (e instanceof AppException && e.code === ResponseCode.ChannelPlatformApiFailed) {\n    logger.error('RedNote token exchange rejected:', e.details?.reason)\n    // surface re-auth requirement to user\n  }\n}","preventionTips":["Keep RedNote client key/secret in env config and validate at startup.","Always use a freshly obtained, single-use authorization code.","Log the platform msg/reason field on every token failure.","Monitor RedNote API changelog for response schema changes."],"tags":["oauth","third-party-api","rednote","token-exchange"],"backgroundTag":"upstream-api-error","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}