{"record":{"id":"78fb2e9c625d1e9a","repo":"yikart/AiToEarn","slug":"15070-78fb2e","errorCode":"15070","errorMessage":"{{platform}} platform API request failed","messagePattern":"(.+?)\\} platform API request failed","errorType":"exception","errorClass":"YouTubePlatformException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/youtube/youtube.service.ts","lineNumber":32,"sourceCode":"import { YoutubeOAuthGrantType, YoutubeSearchOrder, YoutubeSearchType } from './youtube.schema'\n\n@Injectable()\nexport class YoutubeService {\n  private readonly http: AxiosInstance\n\n  constructor(\n    private readonly cfg: YoutubeConfig,\n    private readonly mediaService: MediaService,\n  ) {\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<YouTubeErrorBody>) => {\n        throw YouTubePlatformException.fromAxiosError(error)\n      },\n    )\n    return http\n  }\n\n  private createOAuth2Client() {\n    return new google.auth.OAuth2(\n      this.cfg.clientId,\n      this.cfg.clientSecret,\n      this.cfg.redirectUri,\n    )\n  }\n\n  private createYouTubeClient(accessToken: string) {\n    const auth = new google.auth.OAuth2()\n    auth.setCredentials({ access_token: accessToken })\n    return google.youtube({ version: 'v3', auth })\n  }","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/youtube/youtube.service.ts#L14-L50","documentation":"YouTubeService installs a response interceptor in createHttpClient that converts every failed Axios response into YouTubePlatformException.fromAxiosError. Because the client is created in the constructor, any YouTube API call made through this instance that returns an HTTP error (or network failure) surfaces as code ChannelPlatformApiFailed (15070) with message '{{platform}} platform API request failed'. The underlying status, endpoint, and YouTube error body are preserved in the exception's cause.","triggerScenarios":"Any YouTube Data API request through this HttpClient returning non-2xx: expired/insufficient-scope access tokens (401/403 quotaExceeded/forbidden), quota exceeded (403), invalid video/resource IDs (404), malformed requests (400), rate limiting (429), or network-level Axios failures.","commonSituations":"YouTube OAuth tokens expiring without proactive refresh (401); daily API quota exhausted after heavy uploads (403 quotaExceeded); uploading videos longer than allowed for unverified apps; channel removed or video set private between listing and fetching; calling APIs without the right scope for private resources.","solutions":["Read the exception cause (httpStatus, platformCode like quotaExceeded/invalidGrant) to identify the real failure.","On 401, refresh the access token and retry once; refresh proactively before expiry based on expiresAt.","On 403 quotaExceeded, back off and schedule work outside quota-heavy windows or request a quota increase.","On 404, re-fetch the resource list and treat the item as gone rather than retrying.","Retry only retryable statuses (429, 5xx) with exponential backoff; never retry 400/401/403 non-quota errors."],"exampleFix":"// before\nconst res = await youtubeHttp.get('/youtube/v3/videos', { params })\n// after: handle auth/quota explicitly\ntry {\n  const res = await youtubeHttp.get('/youtube/v3/videos', { params })\n} catch (e) {\n  if (e.httpStatus === 401) return this.withRefreshedToken(() => fetchVideos(params))\n  if (e.cause?.platformCode === 'quotaExceeded') throw new QuotaExceededException()\n  throw e\n}","handlingStrategy":"type-guard","validationCode":"function isYouTubeRequestSafe(accessToken: string, expiresAt?: Date): boolean {\n  return accessToken.length > 0 && (!expiresAt || expiresAt.getTime() > Date.now() + 60_000)\n}","typeGuard":"function isYouTubePlatformError(e: unknown): e is ChannelPlatformException & { cause: { httpStatus?: number; platformCode?: string } } {\n  return e instanceof ChannelPlatformException\n    && e.code === ResponseCode.ChannelPlatformApiFailed\n    && e.cause !== undefined\n}","tryCatchPattern":"try {\n  const res = await youtubeHttp.get('/youtube/v3/videos', { params })\n  return res.data\n} catch (e) {\n  if (!isYouTubePlatformError(e)) throw e\n  const { httpStatus, platformCode } = e.cause\n  if (httpStatus === 401 || platformCode === 'invalidGrant') return refreshAndRetryOnce()\n  if (platformCode === 'quotaExceeded' || httpStatus === 429) throw new QuotaBackoffException(e)\n  if (httpStatus === 404) return null // resource gone\n  throw e\n}","preventionTips":["Refresh YouTube tokens before expiry using expiresAt instead of reacting to 401s.","Branch on the exception's httpStatus/platformCode — the generic message never tells you enough.","Track quota usage (cost per videos.insert/upload is high) and spread heavy jobs across the day.","Only retry 429/5xx with exponential backoff; treat 400/403(non-quota)/404 as terminal."],"tags":["axios","youtube","api-error","interceptor"],"backgroundTag":"youtube-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}