{"record":{"id":"44874d032a76cbe1","repo":"BloopAI/vibe-kanban","slug":"failed-to-fetch-attachment-type-response-sta","errorCode":null,"errorMessage":"Failed to fetch attachment ${type}: ${response.statusText}","messagePattern":"Failed to fetch attachment (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/web-core/src/shared/lib/remoteApi.ts","lineNumber":358,"sourceCode":"  });\n  if (!response.ok) {\n    throw await parseErrorResponse(response, 'Failed to delete attachment');\n  }\n}\n\nexport async function fetchAttachmentSasUrl(\n  attachmentId: string,\n  type: 'file' | 'thumbnail'\n): Promise<string> {\n  const cacheKey = `${attachmentId}:${type}`;\n  const cached = sasUrlCache.get(cacheKey);\n  if (cached && Date.now() < cached.expiresAt) {\n    return cached.url;\n  }\n\n  const response = await makeRequest(`/v1/attachments/${attachmentId}/${type}`);\n  if (!response.ok) {\n    throw new Error(\n      `Failed to fetch attachment ${type}: ${response.statusText}`\n    );\n  }\n\n  const data: AttachmentUrlResponse = await response.json();\n  sasUrlCache.set(cacheKey, {\n    url: data.url,\n    expiresAt: Date.now() + SAS_URL_TTL_MS,\n  });\n  return data.url;\n}\n","sourceCodeStart":340,"sourceCodeEnd":370,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/packages/web-core/src/shared/lib/remoteApi.ts#L340-L370","documentation":"fetchAttachmentSasUrl requests a short-lived SAS (shared access signature) URL for an attachment from /v1/attachments/{id}/{type}. If the response is not ok it throws Error(`Failed to fetch attachment ${type}: ${response.statusText}`). Note the message carries only HTTP statusText, not the server's JSON error body, so it can be empty for HTTP/2 responses.","triggerScenarios":"GET /v1/attachments/:id/:type returns non-ok: attachment id no longer exists, SAS service/storage backend down, 401/403 on the attachment scope, or a network gateway error. `type` is inline in the message (e.g. 'thumbnail' or 'full').","commonSituations":"Opening an old message whose attachment was purged from blob storage; self-hosted deployments where Azure/S3 SAS issuer is misconfigured; expired credentials yielding 403.","solutions":["Check response status in network tab; for 404 treat the attachment as gone and hide/degrade the UI","For 401/403, re-authenticate — makeRequest already retries once on 401, so a persistent 403 means missing permission","Verify the SAS/storage backend configuration on the remote server","Retry with backoff on 5xx, since storage backends may be transiently unavailable"],"exampleFix":"// before\nconst url = await fetchAttachmentSasUrl(id, 'thumbnail');\n// after\nlet url: string;\ntry {\n  url = await fetchAttachmentSasUrl(id, 'thumbnail');\n} catch (e) {\n  url = PLACEHOLDER_IMAGE; // degrade gracefully\n}","handlingStrategy":"fallback","validationCode":"if (!attachmentId || !['thumbnail','full'].includes(type)) return null; // skip request entirely","typeGuard":"function isAttachmentUrlResponse(x: unknown): x is AttachmentUrlResponse { return typeof x === 'object' && x !== null && typeof (x as any).url === 'string' && typeof (x as any).expiresAt === 'string'; }","tryCatchPattern":"let url: string | null = null;\ntry {\n  url = await fetchAttachmentSasUrl(attachmentId, type);\n} catch {\n  url = null; // render placeholder\n}","preventionTips":["Always render a placeholder for attachment images so a failed SAS fetch degrades visually","Respect the sasUrlCache expiry; don't bypass it with manual URL construction","Treat 404 as permanent (attachment deleted) and stop retrying","Retry only on 5xx with backoff"],"tags":["http","attachments","sas-url","network"],"backgroundTag":"http-request-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}