{"record":{"id":"1b78e16c56e15995","repo":"yikart/AiToEarn","slug":"15070-1b78e1","errorCode":"15070","errorMessage":"{{platform}} platform API request failed","messagePattern":"(.+?)\\} platform API request failed","errorType":"exception","errorClass":"LinkedInPlatformException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin.service.ts","lineNumber":37,"sourceCode":"import { LinkedInOAuthGrantType, LinkedInPostLifecycleState } from './linkedin.interface'\nimport { LinkedInDistribution, LinkedInVisibility } from './linkedin.schema'\n\n@Injectable()\nexport class LinkedInService {\n  private readonly http: AxiosInstance\n  private readonly apiBaseUrl = 'https://api.linkedin.com/v2'\n  private readonly restBaseUrl = 'https://api.linkedin.com/rest'\n\n  constructor(private readonly cfg: LinkedinConfig) {\n    this.http = this.createHttpClient()\n  }\n\n  private createHttpClient(): AxiosInstance {\n    const http = axios.create()\n    http.interceptors.response.use(\n      response => response,\n      (error: AxiosError<LinkedInErrorBody>) => {\n        throw LinkedInPlatformException.fromAxiosError(error)\n      },\n    )\n    return http\n  }\n\n  private restHeaders(accessToken: string): Record<string, string> {\n    return {\n      'Authorization': `Bearer ${accessToken}`,\n      'Content-Type': 'application/json',\n      'Linkedin-Version': this.cfg.restVersion,\n      'X-Restli-Protocol-Version': '2.0.0',\n    }\n  }\n\n  generateAuthUrl(scopes: string[], state: string): string {\n    const params = new URLSearchParams({\n      response_type: 'code',\n      client_id: this.cfg.clientId,","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin.service.ts#L19-L55","documentation":"LinkedInService's axios interceptor converts every failed LinkedIn REST API request into a LinkedInPlatformException with ResponseCode.ChannelPlatformApiFailed (15070). Both network-level Axios errors (no response) and non-2xx responses from api.linkedin.com are wrapped; the LinkedIn error body is parsed for platform code/message. Context captured includes endpoint, method, HTTP status, error category, retryability, and the raw LinkedIn payload.","triggerScenarios":"Any call through this.http — OAuth code exchange, fetching member profile (restHeaders-authenticated /rest endpoints), creating UGC posts, uploading images — that returns a non-2xx status (401 invalid token, 403 missing w_member_social or w_organization_social scope, 429 throttle, 400 bad payload) or fails before a response (timeout, DNS, connection refused).","commonSituations":"LinkedIn access token expired (60-day lifetime); app missing the required marketing/community product access; posting as an organization without admin rights; LinkedIn throttling (429) during batch posting; REST API version header mismatch after API version updates; corporate proxy blocking api.linkedin.com.","solutions":["Inspect the thrown LinkedInPlatformException's cause (platformMessage, httpStatus, platformCode) — LinkedIn includes a detailed message and serviceErrorCode in the body.","If httpStatus is 401, refresh the LinkedIn access token (or re-run the OAuth flow) and retry.","If 403, verify the app has requested the required products/scopes (e.g. 'Share on LinkedIn') and that the member/organization has post permission.","If 429 or retryable network/5xx, back off and retry respecting Retry-After.","Confirm the LinkedIn-Version header matches a supported REST API version and the request body conforms to current UGC-post schema."],"exampleFix":"// before: token expired -> 401 wrapped as 15070\nconst res = await this.http.post(`${this.apiBaseUrl}/rest/posts`, body, { headers: this.restHeaders(accessToken) })\n\n// after: refresh on 401 and retry once\ntry {\n  const res = await this.http.post(`${this.apiBaseUrl}/rest/posts`, body, { headers: this.restHeaders(accessToken) })\n} catch (e) {\n  if (e instanceof LinkedInPlatformException && e.cause?.httpStatus === 401) {\n    const fresh = await this.refreshAccessToken(channel)\n    const res = await this.http.post(`${this.apiBaseUrl}/rest/posts`, body, { headers: this.restHeaders(fresh) })\n  } else throw e\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight: token validity and required product/scope confirmation\nif (!accessToken || tokenExpiresAt <= new Date()) await refreshLinkedInToken(channel)\n// member must have granted 'w_member_social' (or org admin for 'w_organization_social') before posting","typeGuard":"function isLinkedInPlatformException(e: unknown): e is LinkedInPlatformException {\n  return e instanceof LinkedInPlatformException\n}","tryCatchPattern":"try {\n  const result = await linkedinService.createPost(accessToken, postBody)\n} catch (e) {\n  if (e instanceof LinkedInPlatformException) {\n    switch (e.cause?.httpStatus) {\n      case 401: /* refresh token, retry once */ break\n      case 403: /* check scopes/products and org admin rights */ break\n      case 429: /* honor Retry-After header before retrying */ break\n      default:\n        if (e.retryable) { /* backoff retry */ } else { /* surface cause.platformMessage */ }\n    }\n  } else throw e\n}","preventionTips":["Re-authenticate LinkedIn channels before the ~60-day token lifetime expires.","Request the 'Share on LinkedIn' product and w_member_social / w_organization_social scopes during app setup.","Confirm organization admin rights before posting as a company page.","Respect LinkedIn throttling limits and Retry-After headers in batch workflows.","Keep the LinkedIn-Version header on a currently supported REST API version and test after version bumps."],"tags":["linkedin","axios-interceptor","http-error","oauth","network"],"backgroundTag":"platform-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}