{"record":{"id":"35b9c23f029b7400","repo":"yikart/AiToEarn","slug":"failed-to-fetch-image-response-status-respon","errorCode":null,"errorMessage":"Failed to fetch image: ${response.status} ${response.statusText}","messagePattern":"Failed to fetch image: (.+?) (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/gemini/gemini.service.ts","lineNumber":207,"sourceCode":"      else if (modality === MediaModality.IMAGE) {\n        result.image = (result.image || 0) + tokenCount\n      }\n      else if (modality === MediaModality.AUDIO) {\n        result.audio = (result.audio || 0) + tokenCount\n      }\n      else if (modality === MediaModality.VIDEO) {\n        result.video = (result.video || 0) + tokenCount\n      }\n    }\n\n    return Object.keys(result).length > 0 ? result : undefined\n  }\n\n  private async fetchImageAsBase64(url: string): Promise<{ base64: string, mimeType: string }> {\n    this.logger.debug({ url }, 'Fetching image as base64')\n    const response = await fetch(url)\n    if (!response.ok) {\n      throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`)\n    }\n    const contentType = response.headers.get('content-type') || 'image/jpeg'\n    const buffer = Buffer.from(await response.arrayBuffer())\n    return {\n      base64: buffer.toString('base64'),\n      mimeType: contentType,\n    }\n  }\n\n  async generateContent(params: GenerateContentParameters): Promise<GenerateContentResponse> {\n    return this.withAvailability('generateContent', async () => {\n      const resolvedParams = await this.resolveRelayJson(params)\n      return await this.genAiClient.models.generateContent(resolvedParams)\n    }, params.model)\n  }\n\n  async generateContentStream(params: GenerateContentParameters): Promise<AsyncGenerator<GenerateContentResponse>> {\n    const resolvedParams = await this.resolveRelayJson(params)","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/gemini/gemini.service.ts#L189-L225","documentation":"fetchImageAsBase64 downloads an image from a URL (used by gemini.service imageData to hydrate image inputs) and throws when the HTTP response is not ok (response.ok false), embedding status and statusText. This is a straightforward network/HTTP failure fetching remote image bytes.","triggerScenarios":"Image URL returns 404/403/410, requires auth or signed access that expired, host is unreachable/DNS fails upstream of fetch (fetch itself may reject first), server returns 5xx, or a redirect to a blocked/expired resource.","commonSituations":"Expired pre-signed S3/OSS URLs; user-supplied image URLs pointing to private or deleted resources; hotlink protection; internal URLs not reachable from the AI service's network.","solutions":["Verify the URL is publicly fetchable (curl -I the URL from the AI service host)","Re-generate signed URLs before use or extend expiry","Serve the image from storage the backend can reach, or send the image as base64 data URI instead of a URL","Add retry with backoff for transient 5xx/network failures, and map this error to a clear user-facing message"],"exampleFix":"// before\nawait geminiService.generateImage({ prompt, imageUrls: [expiredPresignedUrl] })\n// after\nconst freshUrl = await storage.getSignedUrl(key, { expiresIn: 3600 })\nawait geminiService.generateImage({ prompt, imageUrls: [freshUrl] })","handlingStrategy":"retry","validationCode":"async function assertFetchable(url: string): Promise<void> {\n  const res = await fetch(url, { method: 'HEAD' })\n  if (!res.ok) throw new Error(`Image URL not fetchable: ${res.status} ${res.statusText}`)\n}","typeGuard":null,"tryCatchPattern":"try {\n  const img = await geminiService.generateImage({ imageUrls: [url] })\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Failed to fetch image:')) {\n    // refresh signed URL or fall back to base64 input\n    const fresh = await storage.getSignedUrl(key, { expiresIn: 3600 })\n    return geminiService.generateImage({ imageUrls: [fresh] })\n  }\n  throw e\n}","preventionTips":["Generate signed URLs shortly before use with adequate expiry","Verify URLs are reachable from the backend network (not just the client)","Prefer sending images as base64/data URIs for small images","Check user-supplied URLs respond 200 and are public before submitting"],"tags":["network","http","fetch"],"backgroundTag":"http-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}