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
- Check the network tab for the true status code (404 vs 403)
- Re-fetch a fresh SAS URL before downloading instead of reusing a cached one
- For CORS errors, add the storage/CDN origin's allowed origins (Access-Control-Allow-Origin) on the server
- 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
- Refresh the SAS URL immediately before download instead of reusing a cached one
- Configure CORS on the blob/CDN host for the app's origins
- Show disabled download buttons for attachments known to be expired/purged
- Never assume statusText is available — check e.message plus the network tab
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
- Failed to download attachment
- Host returned HTTP ${response.status}
- Auth methods lookup failed (${res.status})
- Session refresh failed. Please sign in again.
- WebRTC offer failed: ${response.status} ${response.statusTex
AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29).
Data as JSON: /api/errors/24f7e92a1b98b00e.
Report an issue: GitHub.