Stirling-Tools/Stirling-PDF · warning · Error

Failed to download avatar: ${response.status} ${response.sta

Error message

Failed to download avatar: ${response.status} ${response.statusText}

What it means

Thrown by downloadAndOptimizeAvatar() when fetch() to the OAuth provider's avatar URL returns a non-OK HTTP status. The error message includes the status code and status text for diagnosis. The fetch uses mode:'cors' and credentials:'omit' to comply with provider CORS policies. The caller syncOAuthAvatar() catches all errors and returns false, so this degrades gracefully to the user's existing picture or initials.

Source

Thrown at frontend/editor/src/saas/services/avatarSyncService.ts:73

  }
}

/**
 * Download and optimize an avatar image
 * Resizes to 256x256 and converts to PNG format
 * @param url Avatar URL from OAuth provider
 * @returns Optimized image blob
 */
export async function downloadAndOptimizeAvatar(url: string): Promise<Blob> {
  try {
    // 1. Fetch image from provider URL
    const response = await fetch(url, {
      mode: "cors",
      credentials: "omit",
    });

    if (!response.ok) {
      throw new Error(
        `Failed to download avatar: ${response.status} ${response.statusText}`,
      );
    }

    const blob = await response.blob();

    // 2. Create image bitmap
    const img = await createImageBitmap(blob);

    // 3. Create canvas and draw scaled image
    const canvas = document.createElement("canvas");
    canvas.width = AVATAR_SIZE;
    canvas.height = AVATAR_SIZE;
    const ctx = canvas.getContext("2d");

    if (!ctx) {
      throw new Error("Failed to get canvas context");
    }

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Check the specific HTTP status code in the error message (403, 404, 429, 5xx)
  2. For expired URLs, re-authenticate via OAuth to get a fresh avatar URL in user_metadata
  3. Add retry with exponential backoff for 429 and 5xx status codes
  4. Fall back to the existing stored avatar or user initials (syncOAuthAvatar already does this)

Example fix

// before
if (!response.ok) {
  throw new Error(`Failed to download avatar: ${response.status} ${response.statusText}`);
}

// after
if (!response.ok) {
  if (response.status === 429 || response.status >= 500) {
    await new Promise((r) => setTimeout(r, 1000));
    return downloadAndOptimizeAvatar(url); // retry once
  }
  throw new Error(`Failed to download avatar: ${response.status} ${response.statusText}`);
}
Defensive patterns

Strategy: fallback

Validate before calling

// Validate the avatar URL is reachable before full processing
// (syncOAuthAvatar already catches and degrades gracefully)
const url = getProviderAvatarUrl(user);
if (!url || !url.startsWith('https://')) {
  return false; // skip sync, use existing avatar
}

Try / catch

// syncOAuthAvatar already wraps in try/catch and returns false on failure:
// The caller should not throw — just check the boolean result:
const synced = await syncOAuthAvatar(user);
if (!synced) {
  // Gracefully degrade to existing avatar or initials — no error UI needed
}

Prevention

When it happens

Trigger: The avatar URL from OAuth provider metadata has expired or changed (GitHub avatar CDN URLs can rotate); the provider rate-limits unauthenticated image fetches (HTTP 429); the URL returns 403/404; CORS policy on the provider's CDN blocks the cross-origin request.

Common situations: Stale avatar URL cached in Supabase user_metadata after the provider rotated CDN links; GitHub/Google avatar CDN under rate limiting; provider image endpoint temporarily down; network proxy/firewall blocking the provider's image domain.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/3d71684bfaf6b026. Report an issue: GitHub.