makeplane/plane · error · Error

Failed to fetch image: ${response.statusText}

Error message

Failed to fetch image: ${response.statusText}

What it means

Thrown by uploadCoverImage in the web cover-image helper when fetch(imageUrl) returns a non-2xx response. The thrown message embeds response.statusText so the HTTP failure reason is visible. This runs before the blob/MIME validation and before uploadUserAsset/uploadFileAsset, so it only covers the source-image fetch step.

Source

Thrown at apps/web/helpers/cover-image.helper.ts:214

/**
 * Uploads a local static image to S3
 */
export const uploadCoverImage = async (
  imageUrl: string,
  uploadConfig: {
    workspaceSlug?: string;
    entityIdentifier: string;
    entityType: EFileAssetType;
    isUserAsset?: boolean;
  }
): Promise<string> => {
  const { workspaceSlug, entityIdentifier, entityType, isUserAsset = false } = uploadConfig;

  // Fetch the local image
  const response = await fetch(imageUrl);

  if (!response.ok) {
    throw new Error(`Failed to fetch image: ${response.statusText}`);
  }

  const blob = await response.blob();

  // Validate it's actually an image
  if (!blob.type.startsWith("image/")) {
    throw new Error("Invalid file type. Please select an image.");
  }

  const fileName = imageUrl.split("/").pop()?.split("?")[0] || "image.jpg";
  const file = new File([blob], fileName, { type: blob.type });

  // Upload based on context
  if (isUserAsset) {
    const uploadResult = await fileService.uploadUserAsset(
      {
        entity_identifier: entityIdentifier,
        entity_type: entityType,

View on GitHub (pinned to 1c8a60f858)

Solutions

  1. Read response.statusText in the thrown message — it tells you 404 vs 403 vs 500.
  2. For Unsplash URLs, verify rate limits and that the host is reachable from the browser.
  3. For local_static covers, confirm the asset still exists in the served static path.
  4. Add CORS/proxy allowances for the image host, or proxy the image through your backend.
  5. Validate the URL (reachable HEAD request) before calling uploadCoverImage.

Example fix

// before
const response = await fetch(imageUrl);
if (!response.ok) throw new Error(`Failed to fetch image: ${response.statusText}`);
// after: include status code and rethrow with cause for better triage
const response = await fetch(imageUrl);
if (!response.ok) {
  throw new Error(`Failed to fetch image (${response.status}): ${response.statusText}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// HEAD-check the URL before fetching the body
const probe = await fetch(imageUrl, { method: 'HEAD' });
if (!probe.ok) throw new Error(`Image URL not reachable (${probe.status})`);

Try / catch

try { await uploadCoverImage(imageUrl, uploadConfig); }
catch (e) {
  if (/Failed to fetch image/.test(e.message)) showCoverImageError(e.message);
  else throw e;
}

Prevention

When it happens

Trigger: The cover image URL is a local_static path that 404s (asset removed/moved); an Unsplash URL that is rate-limited (503) or blocked by CSP/CORS; an external URL whose host returns 403/404; network failure producing a non-OK response; the URL is correct but the server returned 500.

Common situations: Selecting a local static cover whose file was deleted from the bundle; Unsplash quota exceeded; corporate proxy/CORS blocking the image host; mis-typed or stale external URL in a saved page.

Related errors


AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12). Data as JSON: /api/errors/a359791124f085ea. Report an issue: GitHub.