BloopAI/vibe-kanban · error · Error

Failed to fetch attachment ${type}: ${response.statusText}

Error message

Failed to fetch attachment ${type}: ${response.statusText}

What it means

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.

Source

Thrown at packages/web-core/src/shared/lib/remoteApi.ts:358

  });
  if (!response.ok) {
    throw await parseErrorResponse(response, 'Failed to delete attachment');
  }
}

export async function fetchAttachmentSasUrl(
  attachmentId: string,
  type: 'file' | 'thumbnail'
): Promise<string> {
  const cacheKey = `${attachmentId}:${type}`;
  const cached = sasUrlCache.get(cacheKey);
  if (cached && Date.now() < cached.expiresAt) {
    return cached.url;
  }

  const response = await makeRequest(`/v1/attachments/${attachmentId}/${type}`);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch attachment ${type}: ${response.statusText}`
    );
  }

  const data: AttachmentUrlResponse = await response.json();
  sasUrlCache.set(cacheKey, {
    url: data.url,
    expiresAt: Date.now() + SAS_URL_TTL_MS,
  });
  return data.url;
}

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Check response status in network tab; for 404 treat the attachment as gone and hide/degrade the UI
  2. For 401/403, re-authenticate — makeRequest already retries once on 401, so a persistent 403 means missing permission
  3. Verify the SAS/storage backend configuration on the remote server
  4. Retry with backoff on 5xx, since storage backends may be transiently unavailable

Example fix

// before
const url = await fetchAttachmentSasUrl(id, 'thumbnail');
// after
let url: string;
try {
  url = await fetchAttachmentSasUrl(id, 'thumbnail');
} catch (e) {
  url = PLACEHOLDER_IMAGE; // degrade gracefully
}
Defensive patterns

Strategy: fallback

Validate before calling

if (!attachmentId || !['thumbnail','full'].includes(type)) return null; // skip request entirely

Type guard

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'; }

Try / catch

let url: string | null = null;
try {
  url = await fetchAttachmentSasUrl(attachmentId, type);
} catch {
  url = null; // render placeholder
}

Prevention

When it happens

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

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

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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