{"record":{"id":"3d0291eb4258f196","repo":"BloopAI/vibe-kanban","slug":"failed-to-download-attachment","errorCode":null,"errorMessage":"Failed to download attachment","messagePattern":"Failed to download attachment","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/web-core/src/shared/lib/attachmentUtils.ts","lineNumber":13,"sourceCode":"/** Downloads an attachment from a URL and triggers a browser save dialog. */\nexport async function downloadBlobUrl(\n  url: string,\n  filename: string\n): Promise<void> {\n  const response = await fetch(url, {\n    method: 'GET',\n    mode: 'cors',\n    credentials: 'omit',\n  });\n\n  if (!response.ok) {\n    throw new Error('Failed to download attachment');\n  }\n\n  const blob = await response.blob();\n  const objectUrl = URL.createObjectURL(blob);\n\n  try {\n    const anchor = document.createElement('a');\n    anchor.href = objectUrl;\n    anchor.download = filename;\n    document.body.appendChild(anchor);\n    anchor.click();\n    document.body.removeChild(anchor);\n  } finally {\n    URL.revokeObjectURL(objectUrl);\n  }\n}\n\nconst ATTACHMENT_MARKDOWN_PATTERN = /(!?)\\[([^\\]]*)\\]\\(([^)]+)\\)/g;","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/packages/web-core/src/shared/lib/attachmentUtils.ts#L1-L31","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nif (!response.ok) {\n  throw new Error('Failed to download attachment');\n}\n// after\nif (!response.ok) {\n  throw new Error(`Failed to download attachment (HTTP ${response.status})`);\n}","handlingStrategy":"try-catch","validationCode":"const res = await fetch(url, { method: 'HEAD', mode: 'cors', credentials: 'omit' });\nif (!res.ok) throw new Error(`Attachment unavailable (HTTP ${res.status})`);","typeGuard":"function isDownloadError(e: unknown): e is Error & { message: 'Failed to download attachment' } {\n  return e instanceof Error && e.message === 'Failed to download attachment';\n}","tryCatchPattern":"try {\n  await downloadBlobUrl(url, filename);\n} catch (e) {\n  if (isDownloadError(e)) {\n    showToast('Attachment could not be downloaded — it may have been deleted or the link expired');\n    return;\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["network","file-download","cors"],"backgroundTag":"attachment-download-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}