BloopAI/vibe-kanban · error · Error

Failed to download attachment

Error message

Failed to download attachment

What it means

downloadBlobUrl fetches an attachment image (GET, cors, credentials omitted) to build a downloadable blob URL. Any non-ok fetch response throws the opaque 'Failed to download attachment' — the message intentionally does not include status, so the real cause must come from the network tab. Called from handleDownload in the image node UI.

Source

Thrown at packages/ui/src/components/image-node.tsx:97

}

function formatFileSize(bytes: bigint | number | null | undefined): string {
  if (!bytes) return '';
  const num = Number(bytes);
  if (num < 1024) return `${num} B`;
  if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)} KB`;
  return `${(num / (1024 * 1024)).toFixed(1)} MB`;
}

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);
  }
}

function toMetadataFromLocalImage(

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Check the network tab for the true status code (404 vs 403)
  2. Re-fetch a fresh SAS URL before downloading instead of reusing a cached one
  3. For CORS errors, add the storage/CDN origin's allowed origins (Access-Control-Allow-Origin) on the server
  4. Retry once with a fresh URL on failure; show a user-facing download-failed toast otherwise

Example fix

// before
onClick={handleDownload}
// after
const handleDownload = async () => {
  try { await downloadBlobUrl(url, name); }
  catch { notify.error('Download failed — link may have expired, retrying…');
          await downloadBlobUrl(await refreshSasUrl(id), name); }
};
Defensive patterns

Strategy: try-catch

Validate before calling

new URL(url); // throws on malformed attachment URL before fetch
if (!url.startsWith('http')) throw new Error('invalid attachment url');

Type guard

function isHttpUrl(s: string): boolean { try { const u = new URL(s); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } }

Try / catch

try {
  await downloadBlobUrl(url, filename);
} catch {
  notify.error('Download failed. The link may have expired — refresh and try again.');
}

Prevention

When it happens

Trigger: Non-ok response downloading the attachment blob: 404 (blob expired or deleted), 403 (SAS URL expired between render and download), CORS misconfiguration, or network failure to the storage host.

Common situations: User waits on a page long enough for the SAS URL to expire, then clicks download; CDN/storage returning 403 due to missing CORS headers on a custom domain; attachments hosted on a different origin than the app.

Related errors


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