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.

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.

Appendix: 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)