{"record":{"id":"1a13f835ae51cb9d","repo":"BloopAI/vibe-kanban","slug":"failed-to-upload-attachment-errortext","errorCode":null,"errorMessage":"Failed to upload attachment: ${errorText}","messagePattern":"Failed to upload attachment: (.+?)","errorType":"http","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"packages/web-core/src/shared/lib/api.ts","lineNumber":1138,"sourceCode":"    return handleApiResponse<string>(response);\n  },\n};\n\n// Workspace attachments API\nexport const attachmentsApi = {\n  upload: async (attachment: File): Promise<AttachmentResponse> => {\n    const formData = new FormData();\n    formData.append('image', attachment);\n\n    const response = await makeLocalApiRequest('/api/attachments/upload', {\n      method: 'POST',\n      body: formData,\n      credentials: 'include',\n    });\n\n    if (!response.ok) {\n      const errorText = await response.text();\n      throw new ApiError(\n        `Failed to upload attachment: ${errorText}`,\n        response.status,\n        response\n      );\n    }\n\n    return handleApiResponse<AttachmentResponse>(response);\n  },\n\n  uploadForTask: async (\n    taskId: string,\n    attachment: File\n  ): Promise<AttachmentResponse> => {\n    const formData = new FormData();\n    formData.append('image', attachment);\n\n    const response = await makeLocalApiRequest(\n      `/api/attachments/task/${taskId}/upload`,","sourceCodeStart":1120,"sourceCodeEnd":1156,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/packages/web-core/src/shared/lib/api.ts#L1120-L1156","documentation":"attachmentsApi.upload POSTs the file as multipart form data ('image' field) to /api/attachments/upload; on a non-OK response it reads the raw response body as text and throws ApiError('Failed to upload attachment: <server error text>', status, response). The server's error text is embedded directly, so the message content reflects whatever the backend returned (validation error, auth failure, size limit, etc.).","triggerScenarios":"Uploading a file via attachmentsApi.upload when POST /api/attachments/upload returns non-OK: file too large per server limit, disallowed MIME type, missing/invalid auth session cookie, backend attachment storage unavailable, or task/workspace route variant unavailable (this is the generic local endpoint).","commonSituations":"Dragging in a screenshot exceeding the configured max upload size; uploading an unsupported file type (server only accepts images); session cookie expired after backend restart; local backend not running so a proxy/404 error text is returned.","solutions":["Read the embedded errorText in the ApiError message — it contains the server's exact reason (e.g. size limit, bad MIME type)","Compress or resize the file if it exceeds the server's upload limit, or convert to an accepted type","Re-authenticate / log back in if the session cookie expired (401/403 status)","Confirm the backend is running and reachable at the local API base URL"],"exampleFix":"// before\nconst errorText = await response.text();\nthrow new ApiError(`Failed to upload attachment: ${errorText}`, response.status, response);\n// after\nconst errorText = await response.text();\nlet detail = errorText;\ntry { detail = JSON.parse(errorText).message ?? errorText; } catch {}\nthrow new ApiError(\n  `Failed to upload attachment (HTTP ${response.status}): ${detail}`,\n  response.status,\n  response\n);","handlingStrategy":"validation","validationCode":"const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // match server limit\nconst ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];\nif (attachment.size > MAX_UPLOAD_BYTES) return showToast('File too large');\nif (!ACCEPTED_TYPES.includes(attachment.type)) return showToast('Unsupported file type');","typeGuard":"function isUploadError(e: unknown): e is ApiError {\n  return e instanceof ApiError && e.message.startsWith('Failed to upload attachment');\n}","tryCatchPattern":"try {\n  await attachmentsApi.upload(file);\n} catch (e) {\n  if (isUploadError(e)) showToast(`Upload rejected (${e.status}): ${e.message}`);\n  else showToast('Upload failed unexpectedly');\n}","preventionTips":["Check file size and MIME type client-side before uploading","Keep the user's session alive; re-login on 401 before retrying uploads","Show the embedded server errorText to the user rather than hiding it","Confirm the backend is running when uploads fail with proxy-style error text"],"tags":["upload","attachments","api-error","multipart"],"backgroundTag":"file-upload-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}