{"record":{"id":"e9cdc04b66f312a9","repo":"makeplane/plane","slug":"failed-to-fetch-image-response-statustext-e9cdc0","errorCode":null,"errorMessage":"Failed to fetch image: ${response.statusText}","messagePattern":"Failed to fetch image: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/utils/src/file.ts","lineNumber":55,"sourceCode":" * @param {string} url\n * @returns\n */\nexport const getBase64Image = async (url: string): Promise<string> => {\n  if (!url || typeof url !== \"string\") {\n    throw new Error(\"Invalid URL provided\");\n  }\n\n  // Try to create a URL object to validate the URL\n  try {\n    new URL(url);\n  } catch {\n    throw new Error(\"Invalid URL format\");\n  }\n\n  const response = await fetch(url);\n  // check if the response is OK\n  if (!response.ok) {\n    throw new Error(`Failed to fetch image: ${response.statusText}`);\n  }\n\n  const blob = await response.blob();\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n\n    reader.onloadend = () => {\n      if (reader.result) {\n        resolve(reader.result as string);\n      } else {\n        reject(new Error(\"Failed to convert image to base64.\"));\n      }\n    };\n\n    reader.onerror = () => {\n      reject(new Error(\"Failed to read the image file.\"));\n    };\n","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/makeplane/plane/blob/1c8a60f858d8472aa56e29994ec1c7926da2c6ce/packages/utils/src/file.ts#L37-L73","documentation":"Third guard in getBase64Image: after a syntactically valid URL is fetched, response.ok must be true (2xx). Any non-2xx status (404 missing file, 403 forbidden, 500 server error, 502/504 gateway) surfaces as this error with the HTTP statusText. The fetch itself succeeded at the transport level but the server returned an error status.","triggerScenarios":"404 when the avatar/cover file was deleted; 403 when the CDN token expired or CORS policy blocks; 500/502/504 from a failing upstream; 451/410 for removed content.","commonSituations":"Stale avatar URL after re-upload; expired pre-signed URL; rate limiting (429); CORS or auth header issues manifesting as a non-2xx response.","solutions":["Inspect response.status (surfaced only as statusText here) — log the numeric code for diagnosis.","For 403/410 on signed URLs, refresh the URL and retry once.","For 404, fall back to a default image rather than throwing.","For 5xx, implement a single retry with backoff."],"exampleFix":"// before\nconst b64 = await getBase64Image(url);\n\n// after\nlet b64;\ntry {\n  b64 = await getBase64Image(url);\n} catch (e) {\n  // statusText in message; fall back to default avatar\n  b64 = DEFAULT_AVATAR_B64;\n}","handlingStrategy":"fallback","validationCode":"async function fetchOk(url: string): Promise<Response> {\n  const r = await fetch(url);\n  if (!r.ok) throw new Error(`status ${r.status}`);\n  return r;\n}","typeGuard":null,"tryCatchPattern":"try { b64 = await getBase64Image(url); } catch (e) { if (/Failed to fetch image/.test((e as Error).message)) { b64 = DEFAULT_AVATAR_B64; } else throw e; }","preventionTips":["Refresh expired signed URLs before fetching","Fall back to a default image on 404/410","Retry once on transient 5xx"],"tags":["file","network","http-status","url"],"backgroundTag":null,"analyzedSha":"1c8a60f858d8472aa56e29994ec1c7926da2c6ce","analyzedAt":"2026-08-12T14:44:31.636Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}