{"record":{"id":"8685632fca549795","repo":"yikart/AiToEarn","slug":"15070-868563","errorCode":"15070","errorMessage":"{{platform}} platform API request failed","messagePattern":"(.+?)\\} platform API request failed","errorType":"exception","errorClass":"PinterestPlatformException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/pinterest/pinterest.service.ts","lineNumber":45,"sourceCode":"  PinterestPinMediaSourceType,\n} from './pinterest.interface'\n\n@Injectable()\nexport class PinterestService {\n  private readonly http: AxiosInstance\n  private readonly apiBaseUrl: string\n\n  constructor(private readonly cfg: PinterestConfig) {\n    this.apiBaseUrl = this.normalizeApiBaseUrl(cfg.baseUrl)\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<PinterestErrorBody>) => {\n        throw PinterestPlatformException.fromAxiosError(error)\n      },\n    )\n    return http\n  }\n\n  private normalizeApiBaseUrl(baseUrl: string): string {\n    const trimmed = (baseUrl || 'https://api.pinterest.com').replace(/\\/+$/, '')\n    return trimmed.endsWith('/v5') ? trimmed : `${trimmed}/v5`\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      state,\n      response_type: 'code',\n    })","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/pinterest/pinterest.service.ts#L27-L63","documentation":"PinterestService's axios interceptor converts every failed Pinterest API request into a PinterestPlatformException with ResponseCode.ChannelPlatformApiFailed (15070). Network-level Axios failures (no response) and non-2xx responses from api.pinterest.com are both wrapped, with the Pinterest error body parsed for platform code/message. The exception records endpoint, method, HTTP status, category, retryability, and the raw platform payload.","triggerScenarios":"Any request through this.http — OAuth token exchange, fetching user boards, creating pins/media uploads — that returns a non-2xx status (401 invalid token, 403 forbidden board/account, 429 rate limit, 400 invalid pin data) or fails before a response arrives (timeout, DNS, connection refused, TLS failure).","commonSituations":"Pinterest access token expired or user revoked authorization; app missing ads/pins write scopes; publishing to a board the user no longer owns; Pinterest v5 API breaking changes after version bumps; rate limiting during bulk pin creation; server egress blocked to api.pinterest.com.","solutions":["Read the thrown PinterestPlatformException's cause (platformCode, platformMessage, httpStatus) for Pinterest's specific error detail.","If httpStatus is 401, refresh the Pinterest access token or re-run the OAuth flow, then retry.","If 403, verify the token scopes cover pin creation and that the target board belongs to the authenticated account.","If 429 or a retryable network/5xx error, retry with exponential backoff honoring rate-limit headers.","Validate pin payload fields (board_id, media source, link URL) against the Pinterest v5 schema and confirm base URL handling via normalizeApiBaseUrl is correct."],"exampleFix":"// before: expired token -> 401 wrapped as 15070\nconst res = await this.http.post(`${this.apiBaseUrl}/v5/pins`, pinBody, { headers: { Authorization: `Bearer ${token}` } })\n\n// after: refresh on 401 and retry\ntry {\n  const res = await this.http.post(`${this.apiBaseUrl}/v5/pins`, pinBody, { headers: { Authorization: `Bearer ${token}` } })\n} catch (e) {\n  if (e instanceof PinterestPlatformException && e.cause?.httpStatus === 401) {\n    const fresh = await this.refreshAccessToken(channel)\n    const res = await this.http.post(`${this.apiBaseUrl}/v5/pins`, pinBody, { headers: { Authorization: `Bearer ${fresh}` } })\n  } else throw e\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight: token freshness and board ownership check\nif (!accessToken || tokenExpiresAt <= new Date()) await refreshPinterestToken(channel)\nconst boards = await pinterestService.listBoards(accessToken)\nif (!boards.some(b => b.id === targetBoardId)) throw new Error(`Board ${targetBoardId} not accessible for this Pinterest account`)","typeGuard":"function isPinterestPlatformException(e: unknown): e is PinterestPlatformException {\n  return e instanceof PinterestPlatformException\n}","tryCatchPattern":"try {\n  const result = await pinterestService.createPin(accessToken, pinPayload)\n} catch (e) {\n  if (e instanceof PinterestPlatformException) {\n    if (e.cause?.httpStatus === 401) {\n      // refresh token and retry once\n    } else if (e.retryable) {\n      // exponential backoff (429 / 5xx / network)\n    } else {\n      // log cause.platformCode/platformMessage and notify the channel owner\n    }\n  } else throw e\n}","preventionTips":["Refresh Pinterest OAuth tokens before expiry and handle user-initiated revocations by detecting 401 and flagging the channel for re-auth.","Verify board ownership and token write scopes before attempting pin creation.","Rate-limit bulk pin creation and honor Pinterest rate-limit headers to avoid 429s.","Validate pin payloads against the Pinterest v5 API schema; version upgrades often change required fields.","Confirm server egress to api.pinterest.com and correct base URL normalization in deployment configs."],"tags":["pinterest","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"}