BloopAI/vibe-kanban · error

Failed to download attachment

Error message

Failed to download attachment

What it means

downloadBlobUrl performs a GET fetch of an attachment URL with credentials omitted and, if the response is not ok, throws a generic 'Failed to download attachment' Error before creating the blob/object URL used for the save dialog. The error deliberately discards the HTTP status; it only signals the attachment could not be downloaded.

Source

Thrown at packages/web-core/src/shared/lib/attachmentUtils.ts:13

/** Downloads an attachment from a URL and triggers a browser save dialog. */
export async function downloadBlobUrl(
  url: string,
  filename: string
): Promise<void> {
  const response = await fetch(url, {
    method: 'GET',
    mode: 'cors',
    credentials: 'omit',
  });

  if (!response.ok) {
    throw new Error('Failed to download attachment');
  }

  const blob = await response.blob();
  const objectUrl = URL.createObjectURL(blob);

  try {
    const anchor = document.createElement('a');
    anchor.href = objectUrl;
    anchor.download = filename;
    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);
  } finally {
    URL.revokeObjectURL(objectUrl);
  }
}

const ATTACHMENT_MARKDOWN_PATTERN = /(!?)\[([^\]]*)\]\(([^)]+)\)/g;

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Log response.status before throwing (or change the error message to include it) to distinguish 404 vs 403 vs 5xx.
  2. Regenerate the attachment URL (re-fetch the task/message) — presigned URLs expire.
  3. Ensure the attachment host sends Access-Control-Allow-Origin for the app origin, since mode is 'cors' and credentials are omitted.
  4. Check that the attachment ID extracted from markdown still exists via the API before attempting download.

Example fix

// before
if (!response.ok) {
  throw new Error('Failed to download attachment');
}
// after
if (!response.ok) {
  throw new Error(`Failed to download attachment (HTTP ${response.status})`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url, { method: 'HEAD', mode: 'cors', credentials: 'omit' });
if (!res.ok) throw new Error(`Attachment unavailable (HTTP ${res.status})`);

Type guard

function isDownloadError(e: unknown): e is Error & { message: 'Failed to download attachment' } {
  return e instanceof Error && e.message === 'Failed to download attachment';
}

Try / catch

try {
  await downloadBlobUrl(url, filename);
} catch (e) {
  if (isDownloadError(e)) {
    showToast('Attachment could not be downloaded — it may have been deleted or the link expired');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: The attachment URL returns 404 (file deleted or wrong ID), 403 (URL signature expired or credentials needed but credentials:'omit' is set), 5xx from storage backend, or a CORS-blocked request (though CORS failures usually reject fetch itself before this check).

Common situations: Expired presigned S3 URLs in old task messages; attachments referenced in markdown that were removed from storage; cross-origin attachment host lacking CORS headers for mode:'cors'; downloading after the environment/workspace was deleted.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/3d0291eb4258f196. Report an issue: GitHub.