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
- Log response.status before throwing (or change the error message to include it) to distinguish 404 vs 403 vs 5xx.
- Regenerate the attachment URL (re-fetch the task/message) — presigned URLs expire.
- Ensure the attachment host sends Access-Control-Allow-Origin for the app origin, since mode is 'cors' and credentials are omitted.
- 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
- Check attachment existence via API before rendering download links
- Regenerate presigned URLs instead of caching them long-term
- Ensure the attachment host sets CORS headers for the app origin
- Include response.status in the thrown message when customizing this util
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
- 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/3d0291eb4258f196.
Report an issue: GitHub.