{"record":{"id":"f87f91336d8e406a","repo":"yikart/AiToEarn","slug":"15070-f87f91","errorCode":"15070","errorMessage":"{{platform}} platform API request failed","messagePattern":"(.+?)\\} platform API request failed","errorType":"exception","errorClass":"ThreadsPlatformException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/threads/threads.service.ts","lineNumber":43,"sourceCode":"import { ThreadsPlatformException } from './threads.exception'\nimport { ThreadsOAuthGrantType } from './threads.interface'\n\n@Injectable()\nexport class ThreadsService {\n  private readonly logger = new Logger(ThreadsService.name)\n  private readonly apiBaseUrl = 'https://graph.threads.net'\n\n  constructor(private readonly cfg: ThreadsConfig) {}\n\n  private async request<T>(url: string, config: AxiosRequestConfig = {}): Promise<T> {\n    this.logger.debug(`[Threads] ${config.method ?? 'GET'} ${url}`)\n    try {\n      const response: AxiosResponse<T> = await axios(url, config)\n      return response.data\n    }\n    catch (error) {\n      if (axios.isAxiosError<ThreadsErrorBody>(error)) {\n        throw ThreadsPlatformException.fromAxiosError(error)\n      }\n      throw error\n    }\n  }\n\n  generateAuthUrl(scopes: string[], state: string): string {\n    const params = new URLSearchParams({\n      client_id: this.cfg.clientId,\n      redirect_uri: this.cfg.redirectUri,\n      scope: scopes.join(','),\n      response_type: 'code',\n      state,\n    })\n\n    return `https://threads.net/oauth/authorize?${params.toString()}`\n  }\n\n  async exchangeCode(code: string): Promise<{","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/threads/threads.service.ts#L25-L61","documentation":"ThreadsService.request() wraps every raw call to https://graph.threads.net (data, listWorks, createContainer, publishContainer, getContainerStatus, getPublishedPost). Any AxiosError is converted via ThreadsPlatformException.fromAxiosError into a ChannelPlatformException with ResponseCode.ChannelPlatformApiFailed (15070). No Axios response means category Network and retryable=true; a Graph error body is classified by status/Meta error code (auth, rate-limit, media, etc.).","triggerScenarios":"Any Threads Graph API call fails: network/DNS/timeout (error.response undefined), HTTP 4xx/5xx from graph.threads.net, invalid or expired access token, Meta Graph error body (e.g. code 190 invalid token, 4 rate limit, media processing errors), or wrong redirect_uri/client_id during token exchange.","commonSituations":"Expired Threads long-lived token after 60 days; revoked app permissions; Meta app in development mode; Threads API rate limits on publishing bursts; transient network failures or proxy blocks to graph.threads.net; incorrect THREADS_CLIENT_SECRET/redirect URI config.","solutions":["Inspect exception.cause (platformCode, httpStatus, platformMessage, raw) to identify the underlying Meta Graph error","If category is Auth (code 190/expired token), refresh the access token and reconnect the channel","If retryable=true (network or 5xx), retry the request with backoff","For rate-limit errors, slow down publishing and honor retry-after","Verify Threads app config: client id/secret, redirect URI, and requested scopes"],"exampleFix":"// before\nconst data = await threadsService.request(url, config) // throws 15070\n// after\ntry {\n  const data = await threadsService.request(url, config)\n} catch (e) {\n  if (e instanceof ChannelPlatformException && e.cause?.type === PlatformErrorCauseType.Network && e.retryable) {\n    await sleep(backoff); return retry()\n  }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"if (!accessToken) throw new Error('Threads channel is not connected; re-authorize before calling the API')","typeGuard":"function isChannelPlatformException(e: unknown): e is ChannelPlatformException {\n  return e instanceof ChannelPlatformException\n}","tryCatchPattern":"try {\n  const data = await threadsService.request(url, config)\n} catch (e) {\n  if (e instanceof ChannelPlatformException) {\n    if (e.retryable) return retryWithBackoff()\n    if (e.category === PlatformErrorCategory.Auth) return markChannelNeedsReauth()\n    logger.error({ code: e.code, cause: e.cause }, 'Threads API failed')\n  }\n  throw e\n}","preventionTips":["Refresh Threads long-lived tokens before the 60-day expiry","Check exception.retryable and category before deciding to retry","Log exception.cause.raw to capture the exact Meta Graph error code","Monitor rate limits and throttle bulk publishing","Validate token and channel connection state before issuing API calls"],"tags":["threads","meta-graph-api","axios","network","platform-integration"],"backgroundTag":"platform-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}