BloopAI/vibe-kanban · error · ApiError

Failed to upload attachment: ${errorText}

Error message

Failed to upload attachment: ${errorText}

What it means

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.).

Source

Thrown at packages/web-core/src/shared/lib/api.ts:1138

    return handleApiResponse<string>(response);
  },
};

// Workspace attachments API
export const attachmentsApi = {
  upload: async (attachment: File): Promise<AttachmentResponse> => {
    const formData = new FormData();
    formData.append('image', attachment);

    const response = await makeLocalApiRequest('/api/attachments/upload', {
      method: 'POST',
      body: formData,
      credentials: 'include',
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new ApiError(
        `Failed to upload attachment: ${errorText}`,
        response.status,
        response
      );
    }

    return handleApiResponse<AttachmentResponse>(response);
  },

  uploadForTask: async (
    taskId: string,
    attachment: File
  ): Promise<AttachmentResponse> => {
    const formData = new FormData();
    formData.append('image', attachment);

    const response = await makeLocalApiRequest(
      `/api/attachments/task/${taskId}/upload`,

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Read the embedded errorText in the ApiError message — it contains the server's exact reason (e.g. size limit, bad MIME type)
  2. Compress or resize the file if it exceeds the server's upload limit, or convert to an accepted type
  3. Re-authenticate / log back in if the session cookie expired (401/403 status)
  4. Confirm the backend is running and reachable at the local API base URL

Example fix

// before
const errorText = await response.text();
throw new ApiError(`Failed to upload attachment: ${errorText}`, response.status, response);
// after
const errorText = await response.text();
let detail = errorText;
try { detail = JSON.parse(errorText).message ?? errorText; } catch {}
throw new ApiError(
  `Failed to upload attachment (HTTP ${response.status}): ${detail}`,
  response.status,
  response
);
Defensive patterns

Strategy: validation

Validate before calling

const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // match server limit
const ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
if (attachment.size > MAX_UPLOAD_BYTES) return showToast('File too large');
if (!ACCEPTED_TYPES.includes(attachment.type)) return showToast('Unsupported file type');

Type guard

function isUploadError(e: unknown): e is ApiError {
  return e instanceof ApiError && e.message.startsWith('Failed to upload attachment');
}

Try / catch

try {
  await attachmentsApi.upload(file);
} catch (e) {
  if (isUploadError(e)) showToast(`Upload rejected (${e.status}): ${e.message}`);
  else showToast('Upload failed unexpectedly');
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/1a13f835ae51cb9d. Report an issue: GitHub.