{"record":{"id":"01ca4fa5adf120a2","repo":"yikart/AiToEarn","slug":"15070-01ca4f","errorCode":"15070","errorMessage":"{{platform}} platform API request failed","messagePattern":"(.+?)\\} platform API request failed","errorType":"exception","errorClass":"DouyinPlatformException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/douyin.service.ts","lineNumber":68,"sourceCode":"\n  constructor(\n    private readonly cfg: DouyinConfig,\n    private readonly redis: ServerRedisService,\n  ) {\n    this.http = this.createHttpClient()\n  }\n\n  private createHttpClient(): AxiosInstance {\n    const http = axios.create({ baseURL: this.apiBaseUrl })\n    http.interceptors.response.use(\n      (response) => {\n        if (DouyinPlatformException.hasPlatformError(response)) {\n          throw DouyinPlatformException.fromPlatformResponse(response)\n        }\n        return response\n      },\n      (error: AxiosError<DouyinPlatformResponseBody>) => {\n        throw DouyinPlatformException.fromAxiosError(error)\n      },\n    )\n    return http\n  }\n\n  private async apiRequest<T>(\n    method: 'GET' | 'POST',\n    path: string,\n    params: Record<string, string> = {},\n    body?: DouyinApiRequestBody,\n    accessToken?: string,\n  ): Promise<T> {\n    const headers = {\n      ...(accessToken ? { 'access-token': accessToken } : {}),\n      ...(body ? { 'Content-Type': 'application/json' } : {}),\n    }\n\n    const response = await this.http.request<DouyinApiResponse<T>>({","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/douyin.service.ts#L50-L86","documentation":"DouyinService's createHttpClient wraps its Axios instance with the same interceptor pair as the mini-app service: every rejection goes through DouyinPlatformException.fromAxiosError and every 200-with-error-code body through fromPlatformResponse, producing a ChannelPlatformException with message '{{platform}} platform API request failed' (code 15070). It applies to the main Douyin open-platform APIs (OAuth, video publishing, account info). The exception preserves the HTTP status, Douyin platform code, endpoint, and a retryable/category classification.","triggerScenarios":"OAuth token refresh/exchange calls with invalid or expired refresh_token; publishing/upload endpoints returning non-zero error_code (e.g. invalid access_token, rate-limit codes); 4xx/5xx HTTP responses from open.douyin.com; network-level axios rejections (timeout, connection refused, DNS).","commonSituations":"Douyin account authorization revoked or expired so refresh fails with invalid grant; Douyin API rate limits during bulk publishing; misconfigured DOUYIN client_key/client_secret in env; Douyin-side 5xx incidents; corporate proxy blocking outbound HTTPS to open.douyin.com.","solutions":["Check exception.cause.platformCode / httpStatus: auth codes mean re-run the Douyin OAuth connect flow for the account; rate-limit codes mean back off before retrying.","If e.retryable is true, retry with exponential backoff; otherwise surface the error to the channel sync job without retrying.","Use exception.context.endpoint to pinpoint the failing Douyin API and verify required scopes/params for it.","Validate client_key/client_secret and callback URL configuration; confirm outbound access to open.douyin.com from the deployment environment."],"exampleFix":"// before: treating token expiry as transient\ncatch (e) {\n  await this.queue.retry(job)\n}\n\n// after: force reconnect on auth failures, retry only when retryable\ncatch (e) {\n  if (e instanceof ChannelPlatformException) {\n    if (e.cause?.httpStatus === 401) {\n      await this.channels.markNeedsReauth(account)\n      return\n    }\n    if (e.retryable) return this.queue.retry(job)\n  }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight: ensure an account is authorized before publishing jobs\nif (!account.douyinRefreshToken || account.douyinTokenExpiresAt < Date.now() + 60_000) {\n  await refreshDouyinToken(account) // throws classified ChannelPlatformException early\n}","typeGuard":"import { ChannelPlatformException } from '../channels/platforms/platforms.exception'\n\nfunction isDouyinPlatformError(e: unknown): e is ChannelPlatformException {\n  return e instanceof ChannelPlatformException\n    && e.platform === 'douyin'\n    && (e.cause?.platformCode !== undefined || typeof e.cause?.httpStatus === 'number')\n}","tryCatchPattern":"try {\n  return await douyinService.publish(account, post)\n} catch (e) {\n  if (isDouyinPlatformError(e)) {\n    if (e.cause?.httpStatus === 401) {\n      return markAccountNeedsReauth(account.id)\n    }\n    if (e.retryable) return retryWithBackoff(e)\n  }\n  throw e\n}","preventionTips":["Refresh Douyin tokens proactively (refresh_token has a bounded lifetime); treat invalid-grant codes as reconnect-required, never retry.","Throttle publishing to respect Douyin rate limits; queue posts instead of firing bursts.","Validate client_key/client_secret and OAuth redirect URI at startup.","Log exception.context.endpoint so failing APIs are identifiable in production.","Health-check connectivity to open.douyin.com in the deployment environment."],"tags":["douyin","api","http","third-party","oauth","axios"],"backgroundTag":"platform-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}